@editframe/elements 0.34.10-beta → 0.35.0-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.
- package/dist/elements/EFImage.d.ts +6 -0
- package/dist/elements/EFImage.js +16 -1
- package/dist/elements/EFImage.js.map +1 -1
- package/dist/elements/EFText.js +21 -31
- package/dist/elements/EFText.js.map +1 -1
- package/dist/preview/renderTimegroupPreview.js +3 -2
- package/dist/preview/renderTimegroupPreview.js.map +1 -1
- package/package.json +2 -2
|
@@ -20,6 +20,12 @@ declare class EFImage extends EFImage_base implements FrameRenderable {
|
|
|
20
20
|
* @public
|
|
21
21
|
*/
|
|
22
22
|
get renderVersion(): number;
|
|
23
|
+
/**
|
|
24
|
+
* Get whether the image has an alpha channel.
|
|
25
|
+
* Used to determine if we should encode as PNG (alpha) or JPEG (no alpha).
|
|
26
|
+
* @public
|
|
27
|
+
*/
|
|
28
|
+
get hasAlpha(): boolean;
|
|
23
29
|
set assetId(value: string | null);
|
|
24
30
|
get assetId(): string | null;
|
|
25
31
|
render(): lit_html1.TemplateResult<1>;
|
package/dist/elements/EFImage.js
CHANGED
|
@@ -44,6 +44,19 @@ let EFImage = class EFImage$1 extends EFTemporal(EFSourceMixin(FetchMixin(LitEle
|
|
|
44
44
|
get renderVersion() {
|
|
45
45
|
return this.#renderVersion;
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Whether the loaded image has an alpha channel.
|
|
49
|
+
* JPEG images don't have alpha, PNG/WebP may have alpha.
|
|
50
|
+
*/
|
|
51
|
+
#hasAlpha = true;
|
|
52
|
+
/**
|
|
53
|
+
* Get whether the image has an alpha channel.
|
|
54
|
+
* Used to determine if we should encode as PNG (alpha) or JPEG (no alpha).
|
|
55
|
+
* @public
|
|
56
|
+
*/
|
|
57
|
+
get hasAlpha() {
|
|
58
|
+
return this.#hasAlpha;
|
|
59
|
+
}
|
|
47
60
|
#assetId = null;
|
|
48
61
|
set assetId(value) {
|
|
49
62
|
this.#assetId = value;
|
|
@@ -103,6 +116,8 @@ let EFImage = class EFImage$1 extends EFTemporal(EFSourceMixin(FetchMixin(LitEle
|
|
|
103
116
|
const image = new Image();
|
|
104
117
|
const blob = await response.blob();
|
|
105
118
|
signal?.throwIfAborted();
|
|
119
|
+
const mimeType = blob.type.toLowerCase();
|
|
120
|
+
this.#hasAlpha = !mimeType.includes("jpeg") && !mimeType.includes("jpg");
|
|
106
121
|
image.src = URL.createObjectURL(blob);
|
|
107
122
|
await new Promise((resolve, reject) => {
|
|
108
123
|
if (signal?.aborted) {
|
|
@@ -163,8 +178,8 @@ let EFImage = class EFImage$1 extends EFTemporal(EFSourceMixin(FetchMixin(LitEle
|
|
|
163
178
|
if (changedProperties.has("src") || changedProperties.has("assetId")) {
|
|
164
179
|
this.#imageLoaded = false;
|
|
165
180
|
this.loadImage().catch(() => {});
|
|
181
|
+
this.#renderVersion++;
|
|
166
182
|
}
|
|
167
|
-
if (changedProperties.size > 0) this.#renderVersion++;
|
|
168
183
|
}
|
|
169
184
|
/**
|
|
170
185
|
* Get the natural dimensions of the image.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EFImage.js","names":["EFImage","#renderVersion","#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 #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 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 }\n\n // Increment render version on any property change.\n if (changedProperties.size > 0) {\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"],"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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"}
|
package/dist/elements/EFText.js
CHANGED
|
@@ -99,34 +99,28 @@ let EFText = class EFText$1 extends EFTemporal(LitElement) {
|
|
|
99
99
|
await Promise.all(this.segments.map((seg) => seg.updateComplete));
|
|
100
100
|
return this.segments;
|
|
101
101
|
}
|
|
102
|
-
await new Promise((resolve) => requestAnimationFrame(resolve));
|
|
103
102
|
let segments = this.segments;
|
|
104
103
|
if (segments.length > 0) {
|
|
105
104
|
await Promise.all(segments.map((seg) => seg.updateComplete));
|
|
106
|
-
await new Promise((resolve) => requestAnimationFrame(resolve));
|
|
107
|
-
await new Promise((resolve) => requestAnimationFrame(resolve));
|
|
108
105
|
this.#segmentsInitialized = true;
|
|
109
|
-
return
|
|
106
|
+
return segments;
|
|
110
107
|
}
|
|
111
|
-
return new Promise((resolve) => {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
segments
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
108
|
+
return new Promise((resolve, reject) => {
|
|
109
|
+
const timeout = setTimeout(() => {
|
|
110
|
+
const index = this._segmentsReadyResolvers.indexOf(resolveWithSegments);
|
|
111
|
+
if (index > -1) this._segmentsReadyResolvers.splice(index, 1);
|
|
112
|
+
reject(/* @__PURE__ */ new Error("Timeout waiting for text segments to be created"));
|
|
113
|
+
}, 5e3);
|
|
114
|
+
const resolveWithSegments = () => {
|
|
115
|
+
clearTimeout(timeout);
|
|
116
|
+
const segments$1 = this.segments;
|
|
117
|
+
Promise.all(segments$1.map((seg) => seg.updateComplete)).then(() => {
|
|
118
|
+
this.#segmentsInitialized = true;
|
|
119
|
+
resolve(segments$1);
|
|
123
120
|
});
|
|
124
|
-
else if (attempts < maxAttempts) {
|
|
125
|
-
attempts++;
|
|
126
|
-
requestAnimationFrame(checkSegments);
|
|
127
|
-
} else resolve([]);
|
|
128
121
|
};
|
|
129
|
-
|
|
122
|
+
this._segmentsReadyResolvers.push(resolveWithSegments);
|
|
123
|
+
if (segments.length === 0 && this.isConnected) this.splitText();
|
|
130
124
|
});
|
|
131
125
|
}
|
|
132
126
|
connectedCallback() {
|
|
@@ -309,20 +303,16 @@ let EFText = class EFText$1 extends EFTemporal(LitElement) {
|
|
|
309
303
|
}
|
|
310
304
|
this.appendChild(fragment);
|
|
311
305
|
if (templateToPreserve) this.appendChild(templateToPreserve);
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
updateAnimations(this.rootTimegroup || this);
|
|
316
|
-
});
|
|
306
|
+
const segmentElements = this.segments;
|
|
307
|
+
Promise.all(segmentElements.map((seg) => seg.updateComplete)).then(() => {
|
|
308
|
+
updateAnimations(this.rootTimegroup || this);
|
|
317
309
|
});
|
|
318
310
|
this.lastTextContent = text;
|
|
319
311
|
this._textContent = text;
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
resolve();
|
|
323
|
-
});
|
|
324
|
-
this._segmentsReadyResolvers = [];
|
|
312
|
+
this._segmentsReadyResolvers.forEach((resolve) => {
|
|
313
|
+
resolve();
|
|
325
314
|
});
|
|
315
|
+
this._segmentsReadyResolvers = [];
|
|
326
316
|
}
|
|
327
317
|
detectWordBoundaries(text) {
|
|
328
318
|
const boundaries = /* @__PURE__ */ new Map();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EFText.js","names":["EFText","textNodes: ChildNode[]","#segmentsInitialized","wordBoundaries: Map<number, number> | null","currentWordIndex: number | null","currentWordSpan: HTMLSpanElement | null","staggerOffset: number | undefined","wordIndexForStagger: number","result: string[]"],"sources":["../../src/elements/EFText.ts"],"sourcesContent":["import { css, html, LitElement, type PropertyValueMap } from \"lit\";\nimport { customElement, property } from \"lit/decorators.js\";\nimport { durationConverter } from \"./durationConverter.js\";\nimport { EFTemporal } from \"./EFTemporal.js\";\nimport type { EFTimegroup } from \"./EFTimegroup.js\";\nimport { evaluateEasing } from \"./easingUtils.js\";\nimport type { EFTextSegment } from \"./EFTextSegment.js\";\nimport { updateAnimations } from \"./updateAnimations.js\";\nimport type { AnimatableElement } from \"./updateAnimations.js\";\n\nexport type SplitMode = \"line\" | \"word\" | \"char\";\n\n@customElement(\"ef-text\")\nexport class EFText extends EFTemporal(LitElement) {\n static styles = [\n css`\n :host {\n display: inline-flex;\n white-space: normal;\n line-height: 1;\n gap: 0;\n }\n :host([split=\"char\"]) {\n white-space: pre;\n }\n :host([split=\"line\"]) {\n display: flex;\n flex-direction: column;\n }\n ::slotted(*) {\n display: inline-block;\n margin: 0;\n padding: 0;\n }\n .ef-word-wrapper {\n display: inline-block;\n white-space: nowrap;\n }\n `,\n ];\n\n @property({ type: String, reflect: true })\n split: SplitMode = \"word\";\n\n private validateSplit(value: string): SplitMode {\n if (value === \"line\" || value === \"word\" || value === \"char\") {\n return value as SplitMode;\n }\n console.warn(\n `Invalid split value \"${value}\". Must be \"line\", \"word\", or \"char\". Defaulting to \"word\".`,\n );\n return \"word\";\n }\n\n @property({\n type: Number,\n attribute: \"stagger\",\n converter: durationConverter,\n })\n staggerMs?: number;\n\n private validateStagger(value: number | undefined): number | undefined {\n if (value === undefined) return undefined;\n if (value < 0) {\n console.warn(`Invalid stagger value ${value}ms. Must be >= 0. Using 0.`);\n return 0;\n }\n return value;\n }\n\n @property({ type: String, reflect: true })\n easing = \"linear\";\n\n private mutationObserver?: MutationObserver;\n private lastTextContent = \"\";\n private _textContent: string | null = null; // null means not initialized, \"\" means explicitly empty\n private _templateElement: HTMLTemplateElement | null = null;\n private _segmentsReadyResolvers: Array<() => void> = [];\n #segmentsInitialized = false;\n\n render() {\n return html`<slot></slot>`;\n }\n\n // Store text content so we can use it even after DOM is cleared\n set textContent(value: string | null) {\n const newValue = value || \"\";\n // Only update if value actually changed\n if (this._textContent !== newValue) {\n this._textContent = newValue;\n\n // Find template element if not already stored\n if (!this._templateElement && this.isConnected) {\n this._templateElement = this.querySelector(\"template\");\n }\n\n // Clear any existing text nodes\n const textNodes: ChildNode[] = [];\n for (const node of Array.from(this.childNodes)) {\n if (node.nodeType === Node.TEXT_NODE) {\n textNodes.push(node as ChildNode);\n }\n }\n for (const node of textNodes) {\n node.remove();\n }\n // Add new text node if value is not empty\n if (newValue) {\n const textNode = document.createTextNode(newValue);\n this.appendChild(textNode);\n }\n // Trigger re-split\n if (this.isConnected) {\n this.splitText();\n }\n }\n }\n\n get textContent(): string {\n // If _textContent is null, it hasn't been initialized - read from DOM\n if (this._textContent === null) {\n return this.getTextContent();\n }\n // Otherwise use stored value (even if empty string)\n return this._textContent;\n }\n\n /**\n * Get all ef-text-segment elements directly\n * @public\n */\n get segments(): EFTextSegment[] {\n return Array.from(\n this.querySelectorAll(\"ef-text-segment[data-segment-created]\"),\n ) as EFTextSegment[];\n }\n\n /**\n * Returns a promise that resolves when segments are ready (created and connected)\n * Use this to wait for segments after setting textContent or other properties\n * @public\n */\n async whenSegmentsReady(): Promise<EFTextSegment[]> {\n // Wait for text element to be updated first\n await this.updateComplete;\n\n // If no text content, segments will be empty - return immediately\n // Use same logic as splitText to read text content\n const text =\n this._textContent !== null ? this._textContent : this.getTextContent();\n if (!text || text.trim().length === 0) {\n return [];\n }\n\n // If segments already initialized and exist, skip RAF waits\n if (this.#segmentsInitialized && this.segments.length > 0) {\n await Promise.all(this.segments.map((seg) => seg.updateComplete));\n return this.segments;\n }\n\n // First time: wait a frame for splitText to run\n await new Promise((resolve) => requestAnimationFrame(resolve));\n\n // If segments already exist and are connected, wait for updates\n let segments = this.segments;\n if (segments.length > 0) {\n // Wait for all segments to be updated\n await Promise.all(segments.map((seg) => seg.updateComplete));\n // Wait one more frame to ensure connectedCallback has run and properties are set\n await new Promise((resolve) => requestAnimationFrame(resolve));\n // Wait one more frame to ensure properties are fully processed\n await new Promise((resolve) => requestAnimationFrame(resolve));\n // Mark as initialized after first successful wait\n this.#segmentsInitialized = true;\n return this.segments;\n }\n\n // Otherwise, wait for segments to be created (with timeout)\n return new Promise<EFTextSegment[]>((resolve) => {\n let attempts = 0;\n const maxAttempts = 100; // 100 frames = ~1.6 seconds at 60fps\n\n const checkSegments = () => {\n segments = this.segments;\n if (segments.length > 0) {\n // Wait for all segments to be updated\n Promise.all(segments.map((seg) => seg.updateComplete)).then(() => {\n // Wait frames to ensure connectedCallback has run and properties are set\n requestAnimationFrame(() => {\n requestAnimationFrame(() => {\n // Mark as initialized after first successful wait\n this.#segmentsInitialized = true;\n resolve(this.segments);\n });\n });\n });\n } else if (attempts < maxAttempts) {\n attempts++;\n requestAnimationFrame(checkSegments);\n } else {\n // Timeout - return empty array (might be empty text)\n resolve([]);\n }\n };\n checkSegments();\n });\n }\n\n connectedCallback() {\n super.connectedCallback();\n // Find and store template element before any modifications\n this._templateElement = this.querySelector(\"template\");\n\n // Initialize _textContent from DOM if not already set (for declarative usage)\n if (this._textContent === null) {\n this._textContent = this.getTextContent();\n this.lastTextContent = this._textContent;\n }\n\n // Use requestAnimationFrame to ensure DOM is ready\n requestAnimationFrame(() => {\n this.setupMutationObserver();\n this.splitText();\n });\n }\n\n disconnectedCallback() {\n super.disconnectedCallback();\n this.mutationObserver?.disconnect();\n }\n\n protected updated(\n changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>,\n ): void {\n if (\n changedProperties.has(\"split\") ||\n changedProperties.has(\"staggerMs\") ||\n changedProperties.has(\"easing\") ||\n changedProperties.has(\"durationMs\")\n ) {\n this.splitText();\n }\n }\n\n private setupMutationObserver() {\n this.mutationObserver = new MutationObserver(() => {\n // Only react to changes that aren't from our own segment creation\n const currentText = this._textContent || this.getTextContent();\n if (currentText !== this.lastTextContent) {\n this._textContent = currentText;\n this.lastTextContent = currentText;\n this.splitText();\n }\n });\n\n this.mutationObserver.observe(this, {\n childList: true,\n characterData: true,\n subtree: true,\n });\n }\n\n private getTextContent(): string {\n // Get text content, handling both text nodes and HTML content\n let text = \"\";\n for (const node of Array.from(this.childNodes)) {\n if (node.nodeType === Node.TEXT_NODE) {\n text += node.textContent || \"\";\n } else if (node.nodeType === Node.ELEMENT_NODE) {\n const element = node as HTMLElement;\n // Skip ef-text-segment elements (they're created by us)\n if (element.tagName === \"EF-TEXT-SEGMENT\") {\n continue;\n }\n // Skip template elements (they're templates, not content)\n if (element.tagName === \"TEMPLATE\") {\n continue;\n }\n text += element.textContent || \"\";\n }\n }\n return text;\n }\n\n private splitText() {\n // Validate split mode\n const validatedSplit = this.validateSplit(this.split);\n if (validatedSplit !== this.split) {\n this.split = validatedSplit;\n return; // Will trigger updated() which calls splitText() again\n }\n\n // Validate stagger\n const validatedStagger = this.validateStagger(this.staggerMs);\n if (validatedStagger !== this.staggerMs) {\n this.staggerMs = validatedStagger;\n return; // Will trigger updated() which calls splitText() again\n }\n\n // Read text content - use stored _textContent if set, otherwise read from DOM\n const text =\n this._textContent !== null ? this._textContent : this.getTextContent();\n const trimmedText = text.trim();\n const textStartOffset = text.indexOf(trimmedText);\n if (!text || trimmedText.length === 0) {\n // Clear segments if no text\n const existingSegments = Array.from(\n this.querySelectorAll(\"ef-text-segment\"),\n );\n for (const segment of existingSegments) {\n segment.remove();\n }\n // Clear text nodes\n const textNodes: ChildNode[] = [];\n for (const node of Array.from(this.childNodes)) {\n if (node.nodeType === Node.TEXT_NODE) {\n textNodes.push(node as ChildNode);\n }\n }\n for (const node of textNodes) {\n node.remove();\n }\n this.lastTextContent = \"\";\n // Resolve any waiting promises\n this._segmentsReadyResolvers.forEach((resolve) => {\n resolve();\n });\n this._segmentsReadyResolvers = [];\n // Reset initialization flag when clearing segments\n this.#segmentsInitialized = false;\n return;\n }\n\n // Reset initialization flag when we're about to create new segments\n this.#segmentsInitialized = false;\n\n const segments = this.splitTextIntoSegments(text);\n const durationMs = this.durationMs || 1000; // Default 1 second if no duration\n\n // For character mode, detect word boundaries to wrap characters within words\n let wordBoundaries: Map<number, number> | null = null;\n if (this.split === \"char\") {\n wordBoundaries = this.detectWordBoundaries(text);\n }\n\n // Clear ALL child nodes (text nodes and segments) by replacing innerHTML\n // This ensures we don't have any leftover text nodes\n const fragment = document.createDocumentFragment();\n\n // Find template element if not already stored\n if (!this._templateElement) {\n this._templateElement = this.querySelector(\"template\");\n }\n\n // Get template content structure\n // If template exists, clone it; otherwise create default ef-text-segment\n const templateContent = this._templateElement?.content;\n const templateSegments = templateContent\n ? Array.from(templateContent.querySelectorAll(\"ef-text-segment\"))\n : [];\n\n // If no template segments found, we'll create a default one\n const useTemplate = templateSegments.length > 0;\n const segmentsPerTextSegment = useTemplate ? templateSegments.length : 1;\n\n // For character mode with word wrapping, track current word and wrap segments\n let currentWordIndex: number | null = null;\n let currentWordSpan: HTMLSpanElement | null = null;\n let charIndex = 0; // Track position in original text for character mode\n\n // For word splitting, count only word segments (not whitespace) for stagger calculation\n const wordOnlySegments =\n this.split === \"word\"\n ? segments.filter((seg) => !/^\\s+$/.test(seg))\n : segments;\n const wordSegmentCount = wordOnlySegments.length;\n\n // Track word index as we iterate (for word mode with duplicate words)\n // This ensures each occurrence of duplicate words gets a unique stagger index\n let wordStaggerIndex = 0;\n\n // Create new segments in a fragment first\n segments.forEach((segmentText, textIndex) => {\n // Calculate stagger offset if stagger is set\n let staggerOffset: number | undefined;\n if (this.staggerMs !== undefined) {\n // For word splitting, whitespace segments should inherit stagger from preceding word\n const isWhitespace = /^\\s+$/.test(segmentText);\n let wordIndexForStagger: number;\n\n if (this.split === \"word\" && isWhitespace) {\n // Whitespace inherits from the preceding word's index\n // Use the word stagger index (which is the index of the word before this whitespace)\n wordIndexForStagger = Math.max(0, wordStaggerIndex - 1);\n } else if (this.split === \"word\") {\n // For word mode, use the current word stagger index (incremented for each word encountered)\n // This ensures duplicate words get unique indices based on their position\n wordIndexForStagger = wordStaggerIndex;\n wordStaggerIndex++;\n } else {\n // For char/line mode, use the actual position in segments array\n wordIndexForStagger = textIndex;\n }\n\n // Apply easing to the stagger offset\n // Normalize index to 0-1 range (0 for first segment, 1 for last segment)\n const normalizedProgress =\n wordSegmentCount > 1\n ? wordIndexForStagger / (wordSegmentCount - 1)\n : 0;\n\n // Apply easing function to get eased progress\n const easedProgress = evaluateEasing(this.easing, normalizedProgress);\n\n // Calculate total stagger duration (last segment gets full stagger)\n const totalStaggerDuration = (wordSegmentCount - 1) * this.staggerMs;\n\n // Apply eased progress to total stagger duration\n staggerOffset = easedProgress * totalStaggerDuration;\n }\n\n if (useTemplate && templateContent) {\n // Clone template content for each text segment\n // This allows multiple ef-text-segment elements per character/word/line\n const clonedContent = templateContent.cloneNode(\n true,\n ) as DocumentFragment;\n const clonedSegments = Array.from(\n clonedContent.querySelectorAll(\"ef-text-segment\"),\n ) as EFTextSegment[];\n\n clonedSegments.forEach((segment, templateIndex) => {\n // Set properties - Lit will process these when element is connected\n segment.segmentText = segmentText;\n // Calculate segment index accounting for multiple segments per text segment\n segment.segmentIndex =\n textIndex * segmentsPerTextSegment + templateIndex;\n segment.segmentStartMs = 0;\n segment.segmentEndMs = durationMs;\n segment.staggerOffsetMs = staggerOffset ?? 0;\n\n // Set data attribute for line mode to enable block display\n if (this.split === \"line\") {\n segment.setAttribute(\"data-line-segment\", \"true\");\n }\n\n // Mark as created to avoid being picked up as template\n segment.setAttribute(\"data-segment-created\", \"true\");\n\n // For character mode with templates, also wrap in word spans\n if (this.split === \"char\" && wordBoundaries) {\n const originalCharIndex = textStartOffset + charIndex;\n const wordIndex = wordBoundaries.get(originalCharIndex);\n if (wordIndex !== undefined) {\n if (wordIndex !== currentWordIndex) {\n if (currentWordSpan) {\n fragment.appendChild(currentWordSpan);\n }\n currentWordIndex = wordIndex;\n currentWordSpan = document.createElement(\"span\");\n currentWordSpan.className = \"ef-word-wrapper\";\n }\n if (currentWordSpan) {\n currentWordSpan.appendChild(segment);\n } else {\n fragment.appendChild(segment);\n }\n } else {\n if (currentWordSpan) {\n fragment.appendChild(currentWordSpan);\n currentWordSpan = null;\n currentWordIndex = null;\n }\n fragment.appendChild(segment);\n }\n charIndex += segmentText.length;\n } else {\n fragment.appendChild(segment);\n }\n });\n } else {\n // No template - create default ef-text-segment\n const segment = document.createElement(\n \"ef-text-segment\",\n ) as EFTextSegment;\n\n segment.segmentText = segmentText;\n segment.segmentIndex = textIndex;\n segment.segmentStartMs = 0;\n segment.segmentEndMs = durationMs;\n segment.staggerOffsetMs = staggerOffset ?? 0;\n\n // Set data attribute for line mode to enable block display\n if (this.split === \"line\") {\n segment.setAttribute(\"data-line-segment\", \"true\");\n }\n\n // Mark as created to avoid being picked up as template\n segment.setAttribute(\"data-segment-created\", \"true\");\n\n // For character mode, wrap segments within words to prevent line breaks\n if (this.split === \"char\" && wordBoundaries) {\n // Map character index in trimmed text to original text position\n const originalCharIndex = textStartOffset + charIndex;\n const wordIndex = wordBoundaries.get(originalCharIndex);\n if (wordIndex !== undefined) {\n // Check if we're starting a new word\n if (wordIndex !== currentWordIndex) {\n // Close previous word span if it exists\n if (currentWordSpan) {\n fragment.appendChild(currentWordSpan);\n }\n // Start new word span\n currentWordIndex = wordIndex;\n currentWordSpan = document.createElement(\"span\");\n currentWordSpan.className = \"ef-word-wrapper\";\n }\n // Append segment to current word span\n if (currentWordSpan) {\n currentWordSpan.appendChild(segment);\n } else {\n fragment.appendChild(segment);\n }\n } else {\n // Not part of a word (whitespace/punctuation) - append directly\n // Close current word span if it exists\n if (currentWordSpan) {\n fragment.appendChild(currentWordSpan);\n currentWordSpan = null;\n currentWordIndex = null;\n }\n fragment.appendChild(segment);\n }\n // Update character index for next iteration (in trimmed text)\n charIndex += segmentText.length;\n } else {\n // Not character mode or no word boundaries - append directly\n fragment.appendChild(segment);\n }\n }\n });\n\n // Close any remaining word span\n if (this.split === \"char\" && currentWordSpan) {\n fragment.appendChild(currentWordSpan);\n }\n\n // Ensure segments are connected to DOM before checking for animations\n // Append fragment first, then trigger updates\n\n // Replace all children with the fragment (this clears text nodes and old segments)\n // But preserve the template element if it exists\n const templateToPreserve = this._templateElement;\n while (this.firstChild) {\n const child = this.firstChild;\n // Don't remove the template element\n if (child === templateToPreserve) {\n // Skip template, but we need to move it after the fragment\n // So we'll remove it temporarily and re-add it after\n this.removeChild(child);\n continue;\n }\n this.removeChild(child);\n }\n this.appendChild(fragment);\n // Re-add template element if it existed\n if (templateToPreserve) {\n this.appendChild(templateToPreserve);\n }\n\n // Segments will pause their own animations in connectedCallback\n // Lit will automatically update them when they're connected to the DOM\n // Ensure segments are updated after being connected\n requestAnimationFrame(() => {\n const segmentElements = this.segments;\n Promise.all(segmentElements.map((seg) => seg.updateComplete)).then(() => {\n const rootTimegroup = this.rootTimegroup || this;\n updateAnimations(rootTimegroup as AnimatableElement);\n });\n });\n\n this.lastTextContent = text;\n this._textContent = text;\n\n // Resolve any waiting promises after segments are connected\n requestAnimationFrame(() => {\n this._segmentsReadyResolvers.forEach((resolve) => {\n resolve();\n });\n this._segmentsReadyResolvers = [];\n });\n }\n\n private detectWordBoundaries(text: string): Map<number, number> {\n // Create a map from character index to word index\n // Characters within the same word will have the same word index\n const boundaries = new Map<number, number>();\n const trimmedText = text.trim();\n if (!trimmedText) {\n return boundaries;\n }\n\n // Use Intl.Segmenter to detect word boundaries\n const segmenter = new Intl.Segmenter(undefined, {\n granularity: \"word\",\n });\n const segments = Array.from(segmenter.segment(trimmedText));\n\n // Find the offset of trimmedText within the original text\n const textStart = text.indexOf(trimmedText);\n\n let wordIndex = 0;\n for (const seg of segments) {\n if (seg.isWordLike) {\n // Map all character positions in this word to the same word index\n for (let i = 0; i < seg.segment.length; i++) {\n const charPos = textStart + seg.index + i;\n boundaries.set(charPos, wordIndex);\n }\n wordIndex++;\n }\n }\n\n return boundaries;\n }\n\n private splitTextIntoSegments(text: string): string[] {\n // Trim text before segmenting to remove leading/trailing whitespace\n const trimmedText = text.trim();\n if (!trimmedText) {\n return [];\n }\n\n switch (this.split) {\n case \"line\": {\n // Split on newlines and trim each line\n const lines = trimmedText.split(/\\r?\\n/);\n return lines\n .map((line) => line.trim())\n .filter((line) => line.length > 0);\n }\n case \"word\": {\n // Use Intl.Segmenter for locale-aware word segmentation\n const segmenter = new Intl.Segmenter(undefined, {\n granularity: \"word\",\n });\n const segments = Array.from(segmenter.segment(trimmedText));\n const result: string[] = [];\n\n for (const seg of segments) {\n if (seg.isWordLike) {\n // Word-like segment - add it\n result.push(seg.segment);\n } else if (/^\\s+$/.test(seg.segment)) {\n // Whitespace segment - add it as-is\n result.push(seg.segment);\n } else {\n // Punctuation segment - attach to preceding word if it exists\n if (result.length > 0) {\n const lastItem = result[result.length - 1];\n if (lastItem && !/^\\s+$/.test(lastItem)) {\n result[result.length - 1] = lastItem + seg.segment;\n } else {\n result.push(seg.segment);\n }\n } else {\n result.push(seg.segment);\n }\n }\n }\n\n return result;\n }\n case \"char\": {\n // Use Intl.Segmenter for grapheme-aware character segmentation\n const segmenter = new Intl.Segmenter(undefined, {\n granularity: \"grapheme\",\n });\n const segments = Array.from(segmenter.segment(trimmedText));\n return segments.map((seg) => seg.segment);\n }\n default:\n return [trimmedText];\n }\n }\n\n get intrinsicDurationMs(): number | undefined {\n // If explicit duration is set, use it\n if (this.hasExplicitDuration) {\n return undefined; // Let explicit duration take precedence\n }\n\n // If we have a parent timegroup that dictates duration (fixed) or inherits it (fit),\n // we should inherit from it instead of using our intrinsic duration.\n // For 'sequence' and 'contain' modes, the parent relies on our intrinsic duration,\n // so we must provide it.\n if (this.parentTimegroup) {\n const mode = this.parentTimegroup.mode;\n if (mode === \"fixed\" || mode === \"fit\") {\n return undefined;\n }\n }\n\n // Otherwise, calculate from content\n // Use _textContent if set, otherwise read from DOM\n const text =\n this._textContent !== null ? this._textContent : this.getTextContent();\n if (!text || text.trim().length === 0) {\n return 0;\n }\n\n // Use the same splitting logic as splitTextIntoSegments for consistency\n const segments = this.splitTextIntoSegments(text);\n // For word splitting, only count word segments (not whitespace) for intrinsic duration\n const segmentCount =\n this.split === \"word\"\n ? segments.filter((seg) => !/^\\s+$/.test(seg)).length || 1\n : segments.length || 1;\n\n return segmentCount * 1000;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-text\": EFText;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"EFText.js","names":["EFText","textNodes: ChildNode[]","#segmentsInitialized","segments","wordBoundaries: Map<number, number> | null","currentWordIndex: number | null","currentWordSpan: HTMLSpanElement | null","staggerOffset: number | undefined","wordIndexForStagger: number","result: string[]"],"sources":["../../src/elements/EFText.ts"],"sourcesContent":["import { css, html, LitElement, type PropertyValueMap } from \"lit\";\nimport { customElement, property } from \"lit/decorators.js\";\nimport { durationConverter } from \"./durationConverter.js\";\nimport { EFTemporal } from \"./EFTemporal.js\";\nimport type { EFTimegroup } from \"./EFTimegroup.js\";\nimport { evaluateEasing } from \"./easingUtils.js\";\nimport type { EFTextSegment } from \"./EFTextSegment.js\";\nimport { updateAnimations } from \"./updateAnimations.js\";\nimport type { AnimatableElement } from \"./updateAnimations.js\";\n\nexport type SplitMode = \"line\" | \"word\" | \"char\";\n\n@customElement(\"ef-text\")\nexport class EFText extends EFTemporal(LitElement) {\n static styles = [\n css`\n :host {\n display: inline-flex;\n white-space: normal;\n line-height: 1;\n gap: 0;\n }\n :host([split=\"char\"]) {\n white-space: pre;\n }\n :host([split=\"line\"]) {\n display: flex;\n flex-direction: column;\n }\n ::slotted(*) {\n display: inline-block;\n margin: 0;\n padding: 0;\n }\n .ef-word-wrapper {\n display: inline-block;\n white-space: nowrap;\n }\n `,\n ];\n\n @property({ type: String, reflect: true })\n split: SplitMode = \"word\";\n\n private validateSplit(value: string): SplitMode {\n if (value === \"line\" || value === \"word\" || value === \"char\") {\n return value as SplitMode;\n }\n console.warn(\n `Invalid split value \"${value}\". Must be \"line\", \"word\", or \"char\". Defaulting to \"word\".`,\n );\n return \"word\";\n }\n\n @property({\n type: Number,\n attribute: \"stagger\",\n converter: durationConverter,\n })\n staggerMs?: number;\n\n private validateStagger(value: number | undefined): number | undefined {\n if (value === undefined) return undefined;\n if (value < 0) {\n console.warn(`Invalid stagger value ${value}ms. Must be >= 0. Using 0.`);\n return 0;\n }\n return value;\n }\n\n @property({ type: String, reflect: true })\n easing = \"linear\";\n\n private mutationObserver?: MutationObserver;\n private lastTextContent = \"\";\n private _textContent: string | null = null; // null means not initialized, \"\" means explicitly empty\n private _templateElement: HTMLTemplateElement | null = null;\n private _segmentsReadyResolvers: Array<() => void> = [];\n #segmentsInitialized = false;\n\n render() {\n return html`<slot></slot>`;\n }\n\n // Store text content so we can use it even after DOM is cleared\n set textContent(value: string | null) {\n const newValue = value || \"\";\n // Only update if value actually changed\n if (this._textContent !== newValue) {\n this._textContent = newValue;\n\n // Find template element if not already stored\n if (!this._templateElement && this.isConnected) {\n this._templateElement = this.querySelector(\"template\");\n }\n\n // Clear any existing text nodes\n const textNodes: ChildNode[] = [];\n for (const node of Array.from(this.childNodes)) {\n if (node.nodeType === Node.TEXT_NODE) {\n textNodes.push(node as ChildNode);\n }\n }\n for (const node of textNodes) {\n node.remove();\n }\n // Add new text node if value is not empty\n if (newValue) {\n const textNode = document.createTextNode(newValue);\n this.appendChild(textNode);\n }\n // Trigger re-split\n if (this.isConnected) {\n this.splitText();\n }\n }\n }\n\n get textContent(): string {\n // If _textContent is null, it hasn't been initialized - read from DOM\n if (this._textContent === null) {\n return this.getTextContent();\n }\n // Otherwise use stored value (even if empty string)\n return this._textContent;\n }\n\n /**\n * Get all ef-text-segment elements directly\n * @public\n */\n get segments(): EFTextSegment[] {\n return Array.from(\n this.querySelectorAll(\"ef-text-segment[data-segment-created]\"),\n ) as EFTextSegment[];\n }\n\n /**\n * Returns a promise that resolves when segments are ready (created and connected)\n * Use this to wait for segments after setting textContent or other properties\n * @public\n */\n async whenSegmentsReady(): Promise<EFTextSegment[]> {\n // Wait for text element to be updated first\n await this.updateComplete;\n\n // If no text content, segments will be empty - return immediately\n // Use same logic as splitText to read text content\n const text =\n this._textContent !== null ? this._textContent : this.getTextContent();\n if (!text || text.trim().length === 0) {\n return [];\n }\n\n // If segments already initialized and exist, return immediately (no RAF waits)\n if (this.#segmentsInitialized && this.segments.length > 0) {\n await Promise.all(this.segments.map((seg) => seg.updateComplete));\n return this.segments;\n }\n\n // Check if segments are already in DOM (synchronous check - no RAF)\n let segments = this.segments;\n if (segments.length > 0) {\n // Segments exist, just wait for their Lit updates (no RAF)\n await Promise.all(segments.map((seg) => seg.updateComplete));\n this.#segmentsInitialized = true;\n return segments;\n }\n\n // Segments don't exist yet - use the promise-based mechanism (no RAF polling)\n // This waits for splitText() to complete and resolve the promise\n return new Promise<EFTextSegment[]>((resolve, reject) => {\n const timeout = setTimeout(() => {\n // Remove our resolver if we timeout\n const index = this._segmentsReadyResolvers.indexOf(resolveWithSegments);\n if (index > -1) {\n this._segmentsReadyResolvers.splice(index, 1);\n }\n reject(new Error('Timeout waiting for text segments to be created'));\n }, 5000); // 5 second timeout\n\n const resolveWithSegments = () => {\n clearTimeout(timeout);\n // Wait for segment Lit updates\n const segments = this.segments;\n Promise.all(segments.map((seg) => seg.updateComplete)).then(() => {\n this.#segmentsInitialized = true;\n resolve(segments);\n });\n };\n\n this._segmentsReadyResolvers.push(resolveWithSegments);\n \n // Trigger splitText if it hasn't run yet\n // This handles the case where segments haven't been created at all\n if (segments.length === 0 && this.isConnected) {\n this.splitText();\n }\n });\n }\n\n connectedCallback() {\n super.connectedCallback();\n // Find and store template element before any modifications\n this._templateElement = this.querySelector(\"template\");\n\n // Initialize _textContent from DOM if not already set (for declarative usage)\n if (this._textContent === null) {\n this._textContent = this.getTextContent();\n this.lastTextContent = this._textContent;\n }\n\n // Use requestAnimationFrame to ensure DOM is ready\n requestAnimationFrame(() => {\n this.setupMutationObserver();\n this.splitText();\n });\n }\n\n disconnectedCallback() {\n super.disconnectedCallback();\n this.mutationObserver?.disconnect();\n }\n\n protected updated(\n changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>,\n ): void {\n if (\n changedProperties.has(\"split\") ||\n changedProperties.has(\"staggerMs\") ||\n changedProperties.has(\"easing\") ||\n changedProperties.has(\"durationMs\")\n ) {\n this.splitText();\n }\n }\n\n private setupMutationObserver() {\n this.mutationObserver = new MutationObserver(() => {\n // Only react to changes that aren't from our own segment creation\n const currentText = this._textContent || this.getTextContent();\n if (currentText !== this.lastTextContent) {\n this._textContent = currentText;\n this.lastTextContent = currentText;\n this.splitText();\n }\n });\n\n this.mutationObserver.observe(this, {\n childList: true,\n characterData: true,\n subtree: true,\n });\n }\n\n private getTextContent(): string {\n // Get text content, handling both text nodes and HTML content\n let text = \"\";\n for (const node of Array.from(this.childNodes)) {\n if (node.nodeType === Node.TEXT_NODE) {\n text += node.textContent || \"\";\n } else if (node.nodeType === Node.ELEMENT_NODE) {\n const element = node as HTMLElement;\n // Skip ef-text-segment elements (they're created by us)\n if (element.tagName === \"EF-TEXT-SEGMENT\") {\n continue;\n }\n // Skip template elements (they're templates, not content)\n if (element.tagName === \"TEMPLATE\") {\n continue;\n }\n text += element.textContent || \"\";\n }\n }\n return text;\n }\n\n private splitText() {\n // Validate split mode\n const validatedSplit = this.validateSplit(this.split);\n if (validatedSplit !== this.split) {\n this.split = validatedSplit;\n return; // Will trigger updated() which calls splitText() again\n }\n\n // Validate stagger\n const validatedStagger = this.validateStagger(this.staggerMs);\n if (validatedStagger !== this.staggerMs) {\n this.staggerMs = validatedStagger;\n return; // Will trigger updated() which calls splitText() again\n }\n\n // Read text content - use stored _textContent if set, otherwise read from DOM\n const text =\n this._textContent !== null ? this._textContent : this.getTextContent();\n const trimmedText = text.trim();\n const textStartOffset = text.indexOf(trimmedText);\n if (!text || trimmedText.length === 0) {\n // Clear segments if no text\n const existingSegments = Array.from(\n this.querySelectorAll(\"ef-text-segment\"),\n );\n for (const segment of existingSegments) {\n segment.remove();\n }\n // Clear text nodes\n const textNodes: ChildNode[] = [];\n for (const node of Array.from(this.childNodes)) {\n if (node.nodeType === Node.TEXT_NODE) {\n textNodes.push(node as ChildNode);\n }\n }\n for (const node of textNodes) {\n node.remove();\n }\n this.lastTextContent = \"\";\n // Resolve any waiting promises\n this._segmentsReadyResolvers.forEach((resolve) => {\n resolve();\n });\n this._segmentsReadyResolvers = [];\n // Reset initialization flag when clearing segments\n this.#segmentsInitialized = false;\n return;\n }\n\n // Reset initialization flag when we're about to create new segments\n this.#segmentsInitialized = false;\n\n const segments = this.splitTextIntoSegments(text);\n const durationMs = this.durationMs || 1000; // Default 1 second if no duration\n\n // For character mode, detect word boundaries to wrap characters within words\n let wordBoundaries: Map<number, number> | null = null;\n if (this.split === \"char\") {\n wordBoundaries = this.detectWordBoundaries(text);\n }\n\n // Clear ALL child nodes (text nodes and segments) by replacing innerHTML\n // This ensures we don't have any leftover text nodes\n const fragment = document.createDocumentFragment();\n\n // Find template element if not already stored\n if (!this._templateElement) {\n this._templateElement = this.querySelector(\"template\");\n }\n\n // Get template content structure\n // If template exists, clone it; otherwise create default ef-text-segment\n const templateContent = this._templateElement?.content;\n const templateSegments = templateContent\n ? Array.from(templateContent.querySelectorAll(\"ef-text-segment\"))\n : [];\n\n // If no template segments found, we'll create a default one\n const useTemplate = templateSegments.length > 0;\n const segmentsPerTextSegment = useTemplate ? templateSegments.length : 1;\n\n // For character mode with word wrapping, track current word and wrap segments\n let currentWordIndex: number | null = null;\n let currentWordSpan: HTMLSpanElement | null = null;\n let charIndex = 0; // Track position in original text for character mode\n\n // For word splitting, count only word segments (not whitespace) for stagger calculation\n const wordOnlySegments =\n this.split === \"word\"\n ? segments.filter((seg) => !/^\\s+$/.test(seg))\n : segments;\n const wordSegmentCount = wordOnlySegments.length;\n\n // Track word index as we iterate (for word mode with duplicate words)\n // This ensures each occurrence of duplicate words gets a unique stagger index\n let wordStaggerIndex = 0;\n\n // Create new segments in a fragment first\n segments.forEach((segmentText, textIndex) => {\n // Calculate stagger offset if stagger is set\n let staggerOffset: number | undefined;\n if (this.staggerMs !== undefined) {\n // For word splitting, whitespace segments should inherit stagger from preceding word\n const isWhitespace = /^\\s+$/.test(segmentText);\n let wordIndexForStagger: number;\n\n if (this.split === \"word\" && isWhitespace) {\n // Whitespace inherits from the preceding word's index\n // Use the word stagger index (which is the index of the word before this whitespace)\n wordIndexForStagger = Math.max(0, wordStaggerIndex - 1);\n } else if (this.split === \"word\") {\n // For word mode, use the current word stagger index (incremented for each word encountered)\n // This ensures duplicate words get unique indices based on their position\n wordIndexForStagger = wordStaggerIndex;\n wordStaggerIndex++;\n } else {\n // For char/line mode, use the actual position in segments array\n wordIndexForStagger = textIndex;\n }\n\n // Apply easing to the stagger offset\n // Normalize index to 0-1 range (0 for first segment, 1 for last segment)\n const normalizedProgress =\n wordSegmentCount > 1\n ? wordIndexForStagger / (wordSegmentCount - 1)\n : 0;\n\n // Apply easing function to get eased progress\n const easedProgress = evaluateEasing(this.easing, normalizedProgress);\n\n // Calculate total stagger duration (last segment gets full stagger)\n const totalStaggerDuration = (wordSegmentCount - 1) * this.staggerMs;\n\n // Apply eased progress to total stagger duration\n staggerOffset = easedProgress * totalStaggerDuration;\n }\n\n if (useTemplate && templateContent) {\n // Clone template content for each text segment\n // This allows multiple ef-text-segment elements per character/word/line\n const clonedContent = templateContent.cloneNode(\n true,\n ) as DocumentFragment;\n const clonedSegments = Array.from(\n clonedContent.querySelectorAll(\"ef-text-segment\"),\n ) as EFTextSegment[];\n\n clonedSegments.forEach((segment, templateIndex) => {\n // Set properties - Lit will process these when element is connected\n segment.segmentText = segmentText;\n // Calculate segment index accounting for multiple segments per text segment\n segment.segmentIndex =\n textIndex * segmentsPerTextSegment + templateIndex;\n segment.segmentStartMs = 0;\n segment.segmentEndMs = durationMs;\n segment.staggerOffsetMs = staggerOffset ?? 0;\n\n // Set data attribute for line mode to enable block display\n if (this.split === \"line\") {\n segment.setAttribute(\"data-line-segment\", \"true\");\n }\n\n // Mark as created to avoid being picked up as template\n segment.setAttribute(\"data-segment-created\", \"true\");\n\n // For character mode with templates, also wrap in word spans\n if (this.split === \"char\" && wordBoundaries) {\n const originalCharIndex = textStartOffset + charIndex;\n const wordIndex = wordBoundaries.get(originalCharIndex);\n if (wordIndex !== undefined) {\n if (wordIndex !== currentWordIndex) {\n if (currentWordSpan) {\n fragment.appendChild(currentWordSpan);\n }\n currentWordIndex = wordIndex;\n currentWordSpan = document.createElement(\"span\");\n currentWordSpan.className = \"ef-word-wrapper\";\n }\n if (currentWordSpan) {\n currentWordSpan.appendChild(segment);\n } else {\n fragment.appendChild(segment);\n }\n } else {\n if (currentWordSpan) {\n fragment.appendChild(currentWordSpan);\n currentWordSpan = null;\n currentWordIndex = null;\n }\n fragment.appendChild(segment);\n }\n charIndex += segmentText.length;\n } else {\n fragment.appendChild(segment);\n }\n });\n } else {\n // No template - create default ef-text-segment\n const segment = document.createElement(\n \"ef-text-segment\",\n ) as EFTextSegment;\n\n segment.segmentText = segmentText;\n segment.segmentIndex = textIndex;\n segment.segmentStartMs = 0;\n segment.segmentEndMs = durationMs;\n segment.staggerOffsetMs = staggerOffset ?? 0;\n\n // Set data attribute for line mode to enable block display\n if (this.split === \"line\") {\n segment.setAttribute(\"data-line-segment\", \"true\");\n }\n\n // Mark as created to avoid being picked up as template\n segment.setAttribute(\"data-segment-created\", \"true\");\n\n // For character mode, wrap segments within words to prevent line breaks\n if (this.split === \"char\" && wordBoundaries) {\n // Map character index in trimmed text to original text position\n const originalCharIndex = textStartOffset + charIndex;\n const wordIndex = wordBoundaries.get(originalCharIndex);\n if (wordIndex !== undefined) {\n // Check if we're starting a new word\n if (wordIndex !== currentWordIndex) {\n // Close previous word span if it exists\n if (currentWordSpan) {\n fragment.appendChild(currentWordSpan);\n }\n // Start new word span\n currentWordIndex = wordIndex;\n currentWordSpan = document.createElement(\"span\");\n currentWordSpan.className = \"ef-word-wrapper\";\n }\n // Append segment to current word span\n if (currentWordSpan) {\n currentWordSpan.appendChild(segment);\n } else {\n fragment.appendChild(segment);\n }\n } else {\n // Not part of a word (whitespace/punctuation) - append directly\n // Close current word span if it exists\n if (currentWordSpan) {\n fragment.appendChild(currentWordSpan);\n currentWordSpan = null;\n currentWordIndex = null;\n }\n fragment.appendChild(segment);\n }\n // Update character index for next iteration (in trimmed text)\n charIndex += segmentText.length;\n } else {\n // Not character mode or no word boundaries - append directly\n fragment.appendChild(segment);\n }\n }\n });\n\n // Close any remaining word span\n if (this.split === \"char\" && currentWordSpan) {\n fragment.appendChild(currentWordSpan);\n }\n\n // Ensure segments are connected to DOM before checking for animations\n // Append fragment first, then trigger updates\n\n // Replace all children with the fragment (this clears text nodes and old segments)\n // But preserve the template element if it exists\n const templateToPreserve = this._templateElement;\n while (this.firstChild) {\n const child = this.firstChild;\n // Don't remove the template element\n if (child === templateToPreserve) {\n // Skip template, but we need to move it after the fragment\n // So we'll remove it temporarily and re-add it after\n this.removeChild(child);\n continue;\n }\n this.removeChild(child);\n }\n this.appendChild(fragment);\n // Re-add template element if it existed\n if (templateToPreserve) {\n this.appendChild(templateToPreserve);\n }\n\n // Segments will pause their own animations in connectedCallback\n // Lit will automatically update them when they're connected to the DOM\n // Ensure segments are updated after being connected\n const segmentElements = this.segments;\n Promise.all(segmentElements.map((seg) => seg.updateComplete)).then(() => {\n const rootTimegroup = this.rootTimegroup || this;\n updateAnimations(rootTimegroup as AnimatableElement);\n });\n\n this.lastTextContent = text;\n this._textContent = text;\n\n // Resolve any waiting promises after segments are connected (synchronous)\n this._segmentsReadyResolvers.forEach((resolve) => {\n resolve();\n });\n this._segmentsReadyResolvers = [];\n }\n\n private detectWordBoundaries(text: string): Map<number, number> {\n // Create a map from character index to word index\n // Characters within the same word will have the same word index\n const boundaries = new Map<number, number>();\n const trimmedText = text.trim();\n if (!trimmedText) {\n return boundaries;\n }\n\n // Use Intl.Segmenter to detect word boundaries\n const segmenter = new Intl.Segmenter(undefined, {\n granularity: \"word\",\n });\n const segments = Array.from(segmenter.segment(trimmedText));\n\n // Find the offset of trimmedText within the original text\n const textStart = text.indexOf(trimmedText);\n\n let wordIndex = 0;\n for (const seg of segments) {\n if (seg.isWordLike) {\n // Map all character positions in this word to the same word index\n for (let i = 0; i < seg.segment.length; i++) {\n const charPos = textStart + seg.index + i;\n boundaries.set(charPos, wordIndex);\n }\n wordIndex++;\n }\n }\n\n return boundaries;\n }\n\n private splitTextIntoSegments(text: string): string[] {\n // Trim text before segmenting to remove leading/trailing whitespace\n const trimmedText = text.trim();\n if (!trimmedText) {\n return [];\n }\n\n switch (this.split) {\n case \"line\": {\n // Split on newlines and trim each line\n const lines = trimmedText.split(/\\r?\\n/);\n return lines\n .map((line) => line.trim())\n .filter((line) => line.length > 0);\n }\n case \"word\": {\n // Use Intl.Segmenter for locale-aware word segmentation\n const segmenter = new Intl.Segmenter(undefined, {\n granularity: \"word\",\n });\n const segments = Array.from(segmenter.segment(trimmedText));\n const result: string[] = [];\n\n for (const seg of segments) {\n if (seg.isWordLike) {\n // Word-like segment - add it\n result.push(seg.segment);\n } else if (/^\\s+$/.test(seg.segment)) {\n // Whitespace segment - add it as-is\n result.push(seg.segment);\n } else {\n // Punctuation segment - attach to preceding word if it exists\n if (result.length > 0) {\n const lastItem = result[result.length - 1];\n if (lastItem && !/^\\s+$/.test(lastItem)) {\n result[result.length - 1] = lastItem + seg.segment;\n } else {\n result.push(seg.segment);\n }\n } else {\n result.push(seg.segment);\n }\n }\n }\n\n return result;\n }\n case \"char\": {\n // Use Intl.Segmenter for grapheme-aware character segmentation\n const segmenter = new Intl.Segmenter(undefined, {\n granularity: \"grapheme\",\n });\n const segments = Array.from(segmenter.segment(trimmedText));\n return segments.map((seg) => seg.segment);\n }\n default:\n return [trimmedText];\n }\n }\n\n get intrinsicDurationMs(): number | undefined {\n // If explicit duration is set, use it\n if (this.hasExplicitDuration) {\n return undefined; // Let explicit duration take precedence\n }\n\n // If we have a parent timegroup that dictates duration (fixed) or inherits it (fit),\n // we should inherit from it instead of using our intrinsic duration.\n // For 'sequence' and 'contain' modes, the parent relies on our intrinsic duration,\n // so we must provide it.\n if (this.parentTimegroup) {\n const mode = this.parentTimegroup.mode;\n if (mode === \"fixed\" || mode === \"fit\") {\n return undefined;\n }\n }\n\n // Otherwise, calculate from content\n // Use _textContent if set, otherwise read from DOM\n const text =\n this._textContent !== null ? this._textContent : this.getTextContent();\n if (!text || text.trim().length === 0) {\n return 0;\n }\n\n // Use the same splitting logic as splitTextIntoSegments for consistency\n const segments = this.splitTextIntoSegments(text);\n // For word splitting, only count word segments (not whitespace) for intrinsic duration\n const segmentCount =\n this.split === \"word\"\n ? segments.filter((seg) => !/^\\s+$/.test(seg)).length || 1\n : segments.length || 1;\n\n return segmentCount * 1000;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-text\": EFText;\n }\n}\n"],"mappings":"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"}
|
|
@@ -385,7 +385,8 @@ function buildCloneStructure(source, timeMs) {
|
|
|
385
385
|
const clone$1 = document.createElement("canvas");
|
|
386
386
|
clone$1.width = shadowCanvas.width || srcEl.clientWidth;
|
|
387
387
|
clone$1.height = shadowCanvas.height || srcEl.clientHeight;
|
|
388
|
-
if (srcEl.tagName === "EF-
|
|
388
|
+
if (srcEl.tagName === "EF-WAVEFORM") clone$1.dataset.preserveAlpha = "true";
|
|
389
|
+
else if (srcEl.tagName === "EF-IMAGE" && "hasAlpha" in srcEl && srcEl.hasAlpha) clone$1.dataset.preserveAlpha = "true";
|
|
389
390
|
const ctx = clone$1.getContext("2d");
|
|
390
391
|
if (ctx) try {
|
|
391
392
|
ctx.drawImage(shadowCanvas, 0, 0);
|
|
@@ -410,7 +411,7 @@ function buildCloneStructure(source, timeMs) {
|
|
|
410
411
|
const clone$1 = document.createElement("canvas");
|
|
411
412
|
clone$1.width = shadowImg.naturalWidth;
|
|
412
413
|
clone$1.height = shadowImg.naturalHeight;
|
|
413
|
-
clone$1.dataset.preserveAlpha = "true";
|
|
414
|
+
if (srcEl.tagName === "EF-IMAGE" && "hasAlpha" in srcEl && srcEl.hasAlpha) clone$1.dataset.preserveAlpha = "true";
|
|
414
415
|
const ctx = clone$1.getContext("2d");
|
|
415
416
|
if (ctx) try {
|
|
416
417
|
ctx.drawImage(shadowImg, 0, 0);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"renderTimegroupPreview.js","names":["CSS_SAFE_DEFAULT_VALUES: Record<string, string | string[]>","removed: RemovedNodeInfo[]","cs: CSSStyleDeclaration","contentCs: CSSStyleDeclaration | undefined","srcMap: StylePropertyMapReadOnly","node: CloneNode","node","clone","syncState: SyncState","syncStats: SyncStats","rules: string[]"],"sources":["../../src/preview/renderTimegroupPreview.ts"],"sourcesContent":["/**\n * Canvas refresh fix:\n * Canvas pixels must be explicitly cleared and redrawn in syncNodeStyles\n * to ensure video frames are captured correctly during foreignObject rendering.\n * Without clearRect, stale frames from previous seeks may be serialized.\n * \n * See FOREIGNOBJECT_BUG_FIX.md for detailed explanation.\n */\n\nimport { logger } from \"./logger.js\";\n\n/**\n * Elements to skip entirely when building the preview.\n */\nconst SKIP_TAGS = new Set([\n \"EF-AUDIO\",\n \"EF-THUMBNAIL-STRIP\",\n \"EF-FILMSTRIP\",\n \"EF-TIMELINE\",\n \"EF-WORKBENCH\",\n \"SCRIPT\",\n \"STYLE\",\n]);\n\n/**\n * All CSS properties to sync (camelCase for style[] access).\n */\nconst SYNC_PROPERTIES = [\n \"display\", \"visibility\", \"opacity\",\n \"position\", \"top\", \"right\", \"bottom\", \"left\", \"zIndex\",\n \"width\", \"height\", \"minWidth\", \"minHeight\", \"maxWidth\", \"maxHeight\",\n \"flex\", \"flexFlow\", \"justifyContent\", \"alignItems\", \"alignContent\", \"alignSelf\", \"gap\",\n \"gridTemplate\", \"gridColumn\", \"gridRow\", \"gridArea\",\n \"margin\", \"padding\", \"boxSizing\",\n \"border\", \"borderTop\", \"borderRight\", \"borderBottom\", \"borderLeft\", \"borderRadius\",\n \"background\", \"color\", \"boxShadow\", \"filter\", \"backdropFilter\", \"clipPath\",\n \"font\", \"textAlign\", \"textDecoration\", \"textTransform\",\n \"letterSpacing\", \"whiteSpace\", \"textOverflow\", \"lineHeight\",\n \"transform\", \"transformOrigin\", \"transformStyle\",\n \"perspective\", \"perspectiveOrigin\", \"backfaceVisibility\",\n \"cursor\", \"pointerEvents\", \"userSelect\", \"overflow\",\n] as const;\n\n/**\n * Kebab-case versions for computedStyleMap.get() - pre-computed for speed.\n */\nconst SYNC_PROPERTIES_KEBAB = SYNC_PROPERTIES.map(prop =>\n prop.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`)\n);\n\n/**\n * Feature detection: computedStyleMap is ~15% faster for style syncing.\n */\nconst HAS_COMPUTED_STYLE_MAP = typeof Element !== \"undefined\" && typeof Element.prototype.computedStyleMap === \"function\";\n\n/**\n * CSS initial/default values for SAFE-TO-SKIP properties.\n * Only includes NON-INHERITED properties where skipping the default\n * won't affect visual output.\n * \n * EXCLUDED (must always serialize):\n * - Inherited properties (color, font, text-*, visibility, cursor)\n * - Display (affects layout significantly)\n * - Properties where \"auto\" computes to a specific value\n * \n * INCLUDED (safe to skip):\n * - Transform/filter effects (none = no effect)\n * - Box shadows (none = no shadow)\n * - Borders when none/0 (no visual impact)\n * - backdrop-filter (none = no effect)\n */\nconst CSS_SAFE_DEFAULT_VALUES: Record<string, string | string[]> = {\n // Transforms & effects - safe to skip \"none\" (no visual impact)\n transform: \"none\",\n filter: \"none\",\n backdropFilter: \"none\",\n boxShadow: \"none\",\n \n // Borders - safe to skip when none/0\n border: [\"none\", \"0px none\", \"0px\", \"0px none rgb(0, 0, 0)\"],\n borderTop: [\"none\", \"0px none\", \"0px\", \"0px none rgb(0, 0, 0)\"],\n borderRight: [\"none\", \"0px none\", \"0px\", \"0px none rgb(0, 0, 0)\"],\n borderBottom: [\"none\", \"0px none\", \"0px\", \"0px none rgb(0, 0, 0)\"],\n borderLeft: [\"none\", \"0px none\", \"0px\", \"0px none rgb(0, 0, 0)\"],\n borderRadius: [\"0px\", \"0\"],\n \n // Positioning - safe to skip \"static\"\n position: \"static\",\n \n // Z-index - \"auto\" is safe when position is static\n zIndex: \"auto\",\n \n // 3D transforms - safe to skip defaults\n transformStyle: \"flat\",\n perspective: \"none\",\n backfaceVisibility: \"visible\",\n};\n\n/**\n * Check if a value matches a safe-to-skip default.\n */\nfunction isDefaultValue(prop: string, value: string): boolean {\n const defaults = CSS_SAFE_DEFAULT_VALUES[prop];\n if (!defaults) return false;\n if (Array.isArray(defaults)) {\n return defaults.includes(value);\n }\n return defaults === value;\n}\n\n// Re-export temporal types from shared module\nexport {\n type TemporalElement,\n isTemporal,\n getTemporalBounds,\n isVisibleAtTime,\n} from \"./previewTypes.js\";\n\n// Import for internal use\nimport {\n getTemporalBounds,\n} from \"./previewTypes.js\";\n\n/**\n * Tree node representing a source/clone pair with children.\n * This replaces the flat array approach for cleaner recursive traversal.\n */\nexport interface CloneNode {\n source: Element;\n clone: HTMLElement;\n children: CloneNode[];\n isCanvasClone: boolean;\n /** Cached temporal bounds for this node */\n bounds: { startMs: number; endMs: number };\n /** Parent node reference for ancestor visibility checks */\n parent: CloneNode | null;\n}\n\n/** Tree-based sync state */\nexport interface CloneTree {\n root: CloneNode | null;\n}\n\n/** Sync state with tree structure and delta tracking */\nexport interface SyncState {\n tree: CloneTree;\n nodeCount: number; // Total number of nodes (for debugging/logging)\n /** Maps clone canvases to their original source elements (ef-video, ef-image, etc.) */\n canvasSourceMap: WeakMap<HTMLCanvasElement, Element>;\n /** Previous frame's visible set for delta tracking */\n previousVisibleSet: Set<CloneNode>;\n /** Current frame's visible set (updated by syncStyles) */\n currentVisibleSet: Set<CloneNode>;\n}\n\n/** Info needed to restore a removed node */\ninterface RemovedNodeInfo {\n node: CloneNode;\n parent: Node;\n nextSibling: Node | null;\n}\n\n/**\n * Remove hidden nodes from the clone DOM for serialization.\n * Returns info needed to restore them afterward.\n * \n * This physically removes non-visible nodes so they won't be serialized,\n * avoiding the cost of serializing hidden elements and their resources.\n */\nexport function removeHiddenNodesForSerialization(state: SyncState): RemovedNodeInfo[] {\n const removed: RemovedNodeInfo[] = [];\n const visibleSet = state.currentVisibleSet;\n \n // Traverse all nodes and remove those not in visible set\n function visit(node: CloneNode): void {\n // First recurse to children (before potentially removing this node)\n for (const child of node.children) {\n visit(child);\n }\n \n // If this node isn't visible, remove it from DOM\n if (!visibleSet.has(node)) {\n const parent = node.clone.parentNode;\n if (parent) {\n const nextSibling = node.clone.nextSibling;\n parent.removeChild(node.clone);\n removed.push({ node, parent, nextSibling });\n }\n }\n }\n \n if (state.tree.root) {\n visit(state.tree.root);\n }\n \n return removed;\n}\n\n/**\n * Restore previously removed hidden nodes to the clone DOM.\n * Must be called after serialization to maintain tree integrity for next frame.\n */\nexport function restoreHiddenNodes(removed: RemovedNodeInfo[]): void {\n // Restore in reverse order to maintain correct DOM positions\n for (let i = removed.length - 1; i >= 0; i--) {\n const { node, parent, nextSibling } = removed[i]!;\n if (nextSibling) {\n parent.insertBefore(node.clone, nextSibling);\n } else {\n parent.appendChild(node.clone);\n }\n }\n}\n\n/**\n * Get visible canvases from the current visible set.\n * Use this to skip encoding hidden canvases during serialization.\n */\nexport function getVisibleCanvases(state: SyncState): Set<HTMLCanvasElement> {\n const visibleCanvases = new Set<HTMLCanvasElement>();\n for (const node of state.currentVisibleSet) {\n if (node.clone instanceof HTMLCanvasElement) {\n visibleCanvases.add(node.clone);\n }\n }\n return visibleCanvases;\n}\n\n/**\n * Traverse all nodes in the clone tree, calling the callback for each.\n */\nexport function traverseCloneTree(state: SyncState, callback: (node: CloneNode) => void): void {\n function visit(node: CloneNode): void {\n callback(node);\n for (const child of node.children) {\n visit(child);\n }\n }\n if (state.tree.root) {\n visit(state.tree.root);\n }\n}\n\n/**\n * Unified CSS property sync for all elements (canvas clones and regular elements).\n * \n * Canvas clones use a limited property set matching the original implementation\n * to avoid dimension/layout issues. Regular elements use the full SYNC_PROPERTIES array.\n * \n * @param source - Source element to read styles from\n * @param clone - Clone element to write styles to\n * @param contentSource - Optional content element for width/height (canvas clones only)\n */\nfunction syncElementStyles(\n source: Element,\n clone: HTMLElement,\n contentSource?: Element,\n): void {\n const cloneStyle = clone.style as any;\n const tagName = (source as HTMLElement).tagName;\n const isCanvasClone = !!contentSource;\n \n // Canvas clones: Use exact property list from original implementation\n if (isCanvasClone) {\n let cs: CSSStyleDeclaration;\n let contentCs: CSSStyleDeclaration | undefined;\n \n try {\n cs = getComputedStyle(source);\n if (contentSource) {\n contentCs = getComputedStyle(contentSource);\n }\n } catch { return; }\n \n // Exact properties from original copyCanvasCloneStyles + syncNodeStyles\n cloneStyle.position = cs.position;\n cloneStyle.top = cs.top;\n cloneStyle.right = cs.right;\n cloneStyle.bottom = cs.bottom;\n cloneStyle.left = cs.left;\n cloneStyle.margin = cs.margin;\n cloneStyle.zIndex = cs.zIndex;\n cloneStyle.transform = cs.transform;\n cloneStyle.transformOrigin = cs.transformOrigin;\n cloneStyle.opacity = cs.opacity;\n cloneStyle.visibility = cs.visibility;\n cloneStyle.backfaceVisibility = cs.backfaceVisibility;\n cloneStyle.transformStyle = cs.transformStyle;\n \n // Width/height from content source (shadow canvas/img)\n if (contentCs) {\n cloneStyle.width = contentCs.width;\n cloneStyle.height = contentCs.height;\n }\n \n cloneStyle.display = \"block\";\n cloneStyle.animation = \"none\";\n cloneStyle.transition = \"none\";\n \n return;\n }\n \n // Regular elements: full property sync from SYNC_PROPERTIES\n const propLen = SYNC_PROPERTIES.length;\n \n if (HAS_COMPUTED_STYLE_MAP) {\n let srcMap: StylePropertyMapReadOnly;\n \n try {\n srcMap = source.computedStyleMap();\n } catch { return; }\n \n for (let j = 0; j < propLen; j++) {\n const kebab = SYNC_PROPERTIES_KEBAB[j]!;\n const camel = SYNC_PROPERTIES[j]!;\n \n const srcVal = srcMap.get(kebab);\n if (!srcVal) continue;\n \n const strVal = srcVal.toString();\n \n if (camel === \"display\") {\n // For caption child elements, preserve display:none when explicitly set\n // (they use it to hide empty content, not for temporal visibility)\n const isCaptionChild = tagName && (\n tagName === 'EF-CAPTIONS-ACTIVE-WORD' ||\n tagName === 'EF-CAPTIONS-BEFORE-ACTIVE-WORD' ||\n tagName === 'EF-CAPTIONS-AFTER-ACTIVE-WORD' ||\n tagName === 'EF-CAPTIONS-SEGMENT'\n );\n const targetDisplay = (strVal === \"none\" && !isCaptionChild) ? \"block\" : strVal;\n cloneStyle.display = targetDisplay;\n continue;\n }\n \n // Skip clipPath - clones always have clipPath: none for rendering\n // (source may have clip-path: inset(100%) from proxy mode)\n if (camel === \"clipPath\") continue;\n \n // OPTIMIZATION: Skip default values to reduce serialized HTML size\n // If the computed value is the CSS default, don't set it as inline style\n if (isDefaultValue(camel, strVal)) {\n // Remove from inline style if it was previously set\n if (cloneStyle[camel]) cloneStyle[camel] = \"\";\n continue;\n }\n \n cloneStyle[camel] = strVal;\n }\n } else {\n let cs: CSSStyleDeclaration;\n \n try {\n cs = getComputedStyle(source);\n } catch { return; }\n \n const srcStyle = cs as any;\n \n for (const prop of SYNC_PROPERTIES) {\n const srcVal = srcStyle[prop];\n if (!srcVal) continue;\n \n if (prop === \"display\") {\n // For caption child elements, preserve display:none when explicitly set\n // (they use it to hide empty content, not for temporal visibility)\n const isCaptionChild = tagName && (\n tagName === 'EF-CAPTIONS-ACTIVE-WORD' ||\n tagName === 'EF-CAPTIONS-BEFORE-ACTIVE-WORD' ||\n tagName === 'EF-CAPTIONS-AFTER-ACTIVE-WORD' ||\n tagName === 'EF-CAPTIONS-SEGMENT'\n );\n const targetDisplay = (srcVal === \"none\" && !isCaptionChild) ? \"block\" : srcVal;\n cloneStyle.display = targetDisplay;\n continue;\n }\n \n // Skip clipPath - clones always have clipPath: none for rendering\n // (source may have clip-path: inset(100%) from proxy mode)\n if (prop === \"clipPath\") continue;\n \n // OPTIMIZATION: Skip default values to reduce serialized HTML size\n if (isDefaultValue(prop, srcVal)) {\n if (cloneStyle[prop]) cloneStyle[prop] = \"\";\n continue;\n }\n \n cloneStyle[prop] = srcVal;\n }\n }\n \n // Disable animations/transitions to prevent re-animation\n cloneStyle.animation = \"none\";\n cloneStyle.transition = \"none\";\n}\n\n/**\n * Refresh canvas pixel content from shadow DOM source.\n * Handles both shadow canvas and shadow img sources.\n */\nfunction refreshCanvasPixels(node: CloneNode): void {\n const { source, clone } = node;\n const canvas = clone as HTMLCanvasElement;\n const shadowCanvas = source.shadowRoot?.querySelector(\"canvas\");\n const shadowImg = source.shadowRoot?.querySelector(\"img\");\n \n if (shadowCanvas) {\n // Update buffer dimensions if needed\n if (canvas.width !== shadowCanvas.width) canvas.width = shadowCanvas.width;\n if (canvas.height !== shadowCanvas.height) canvas.height = shadowCanvas.height;\n \n // Copy pixels with explicit clear\n const ctx = canvas.getContext(\"2d\");\n if (ctx && shadowCanvas.width > 0 && shadowCanvas.height > 0) {\n try {\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n ctx.drawImage(shadowCanvas, 0, 0);\n } catch (e) {\n logger.warn(\"[refreshCanvasPixels] Canvas draw failed:\", e);\n }\n }\n } else if (shadowImg?.complete && shadowImg.naturalWidth > 0) {\n // Update buffer dimensions if needed\n if (canvas.width !== shadowImg.naturalWidth) canvas.width = shadowImg.naturalWidth;\n if (canvas.height !== shadowImg.naturalHeight) canvas.height = shadowImg.naturalHeight;\n \n // Copy pixels with explicit clear\n const ctx = canvas.getContext(\"2d\");\n if (ctx) {\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n try { ctx.drawImage(shadowImg, 0, 0); } catch {}\n }\n }\n}\n\n/**\n * Sync text content from light DOM to clone.\n */\nfunction syncTextContent(source: Element, clone: HTMLElement): void {\n const srcTextNode = source.childNodes[0];\n if (srcTextNode?.nodeType === Node.TEXT_NODE) {\n const srcText = srcTextNode.textContent || \"\";\n const cloneTextNode = clone.childNodes[0];\n \n if (cloneTextNode?.nodeType === Node.TEXT_NODE) {\n // Update existing text node\n if (cloneTextNode.textContent !== srcText) cloneTextNode.textContent = srcText;\n } else if (!clone.childNodes.length) {\n // Only create text node if clone has NO children (was empty when initially cloned)\n // Don't set textContent as it would delete element children!\n clone.appendChild(document.createTextNode(srcText));\n }\n }\n}\n\n/**\n * Sync input element value.\n */\nfunction syncInputValue(source: Element, clone: HTMLElement): void {\n if (source instanceof HTMLInputElement) {\n const srcVal = source.value;\n const cloneInput = clone as HTMLInputElement;\n if (cloneInput.value !== srcVal) {\n cloneInput.value = srcVal;\n cloneInput.setAttribute(\"value\", srcVal);\n }\n }\n}\n\n/**\n * Build clone tree structure with minimal overhead.\n * Caches temporal bounds on each node for visibility checks.\n * Optionally syncs styles in the same pass if timeMs is provided.\n */\nexport function buildCloneStructure(source: Element, timeMs?: number): {\n container: HTMLDivElement;\n syncState: SyncState;\n} {\n const container = document.createElement(\"div\");\n container.style.cssText = \"position:absolute;top:0;left:0;width:100%;height:100%\";\n \n let nodeCount = 0;\n const canvasSourceMap = new WeakMap<HTMLCanvasElement, Element>();\n \n function cloneElement(srcEl: Element, parentNode: CloneNode | null): CloneNode | null {\n if (SKIP_TAGS.has(srcEl.tagName)) return null;\n \n // Get temporal bounds upfront for indexing\n const bounds = getTemporalBounds(srcEl);\n \n // SVG - clone entire subtree (no children tracking needed)\n if (srcEl instanceof SVGElement) {\n const svgClone = srcEl.cloneNode(true) as SVGElement;\n const node: CloneNode = {\n source: srcEl,\n clone: svgClone as unknown as HTMLElement,\n children: [],\n isCanvasClone: false,\n bounds,\n parent: parentNode,\n };\n nodeCount++;\n return node;\n }\n \n // Canvas - copy pixels\n if (srcEl instanceof HTMLCanvasElement) {\n const canvas = document.createElement(\"canvas\");\n canvas.width = srcEl.width;\n canvas.height = srcEl.height;\n const ctx = canvas.getContext(\"2d\");\n if (ctx) {\n try { ctx.drawImage(srcEl, 0, 0); } catch {}\n }\n // Raw canvas elements don't need style syncing, just return clone\n // return null;\n }\n \n // Custom elements with shadow canvas (e.g., ef-video, ef-image)\n const isCustom = srcEl.tagName.includes(\"-\");\n if (isCustom && srcEl.shadowRoot) {\n const shadowCanvas = srcEl.shadowRoot.querySelector(\"canvas\");\n if (shadowCanvas) {\n const clone = document.createElement(\"canvas\");\n clone.width = shadowCanvas.width || srcEl.clientWidth;\n clone.height = shadowCanvas.height || srcEl.clientHeight;\n // Mark ef-image canvases to preserve transparency (use PNG instead of JPEG)\n // ef-video doesn't need this since videos don't have transparency\n if (srcEl.tagName === \"EF-IMAGE\" || srcEl.tagName === \"EF-WAVEFORM\") {\n clone.dataset.preserveAlpha = \"true\";\n }\n \n const ctx = clone.getContext(\"2d\");\n if (ctx) {\n try { ctx.drawImage(shadowCanvas, 0, 0); } catch {}\n }\n \n // Copy initial CSS styles using unified sync\n // Pass shadowCanvas as contentSource for width/height\n try {\n syncElementStyles(srcEl, clone, shadowCanvas);\n } catch {}\n \n // Map clone canvas to source element for RenderContext caching\n canvasSourceMap.set(clone, srcEl);\n \n const node: CloneNode = {\n source: srcEl,\n clone,\n children: [],\n isCanvasClone: true,\n bounds,\n parent: parentNode,\n };\n nodeCount++;\n return node;\n }\n \n const shadowImg = srcEl.shadowRoot.querySelector(\"img\");\n if (shadowImg?.complete && shadowImg.naturalWidth > 0) {\n const clone = document.createElement(\"canvas\");\n clone.width = shadowImg.naturalWidth;\n clone.height = shadowImg.naturalHeight;\n // Mark as image-sourced canvas to preserve transparency (use PNG instead of JPEG)\n clone.dataset.preserveAlpha = \"true\";\n const ctx = clone.getContext(\"2d\");\n if (ctx) {\n try { ctx.drawImage(shadowImg, 0, 0); } catch {}\n }\n \n // Copy initial CSS styles using unified sync\n // Pass shadowImg as contentSource for width/height\n try {\n syncElementStyles(srcEl, clone, shadowImg);\n } catch {}\n \n // Map clone canvas to source element for RenderContext caching\n canvasSourceMap.set(clone, srcEl);\n \n const node: CloneNode = {\n source: srcEl,\n clone,\n children: [],\n isCanvasClone: true,\n bounds,\n parent: parentNode,\n };\n nodeCount++;\n return node;\n }\n }\n \n // Standard element clone\n const clone = document.createElement(isCustom ? \"div\" : srcEl.tagName.toLowerCase()) as HTMLElement;\n \n // Copy attributes - OPTIMIZATION: Early exit if no attributes\n const attrs = srcEl.attributes;\n const attrLen = attrs.length;\n if (attrLen > 0) {\n for (let i = 0; i < attrLen; i++) {\n const attr = attrs[i]!;\n const name = attr.name.toLowerCase();\n if (name === \"id\" || name.startsWith(\"on\")) continue;\n if (isCustom && name !== \"class\" && !name.startsWith(\"data-\")) continue;\n try { clone.setAttribute(attr.name, attr.value); } catch {}\n }\n }\n \n if (srcEl instanceof HTMLImageElement && srcEl.src) {\n (clone as HTMLImageElement).src = srcEl.src;\n }\n if (srcEl instanceof HTMLInputElement) {\n (clone as HTMLInputElement).value = srcEl.value;\n }\n \n const node: CloneNode = {\n source: srcEl,\n clone,\n children: [],\n isCanvasClone: false,\n bounds,\n parent: parentNode,\n };\n nodeCount++;\n \n // Shadow DOM children - OPTIMIZATION: Early exit if no childNodes\n if (srcEl.shadowRoot) {\n const shadowChildren = srcEl.shadowRoot.childNodes;\n const shadowLen = shadowChildren.length;\n if (shadowLen > 0) {\n // For text segments, ALWAYS create a text node placeholder even if empty.\n // Caption elements now use light DOM, so they don't need special handling here.\n const isTextSegment = srcEl.tagName === 'EF-TEXT-SEGMENT';\n let hasTextNode = false;\n \n for (let i = 0; i < shadowLen; i++) {\n const child = shadowChildren[i]!;\n if (child.nodeType === Node.TEXT_NODE) {\n const text = child.textContent?.trim();\n // Always include text for text segments (even if whitespace-only, e.g., \" \")\n if (text || isTextSegment) {\n clone.appendChild(document.createTextNode(child.textContent || \"\"));\n hasTextNode = true;\n }\n } else if (child.nodeType === Node.ELEMENT_NODE) {\n const el = child as Element;\n if (el.tagName === \"STYLE\" || el.tagName === \"SLOT\") continue;\n const childNode = cloneElement(el, node);\n if (childNode) {\n node.children.push(childNode);\n clone.appendChild(childNode.clone);\n }\n }\n }\n \n // For text segments, ensure there's always a text node for syncStyles to update\n if (isTextSegment && !hasTextNode) {\n clone.appendChild(document.createTextNode(\"\"));\n }\n }\n }\n \n // Light DOM children - OPTIMIZATION: Use indexed loop for performance\n const lightChildren = srcEl.childNodes;\n const lightLen = lightChildren.length;\n for (let i = 0; i < lightLen; i++) {\n const child = lightChildren[i]!;\n if (child.nodeType === Node.TEXT_NODE) {\n const text = child.textContent?.trim();\n if (text) clone.appendChild(document.createTextNode(text));\n } else if (child.nodeType === Node.ELEMENT_NODE) {\n const childNode = cloneElement(child as Element, node);\n if (childNode) {\n node.children.push(childNode);\n clone.appendChild(childNode.clone);\n }\n }\n }\n \n return node;\n }\n \n const root = cloneElement(source, null);\n if (root) container.appendChild(root.clone);\n \n const syncState: SyncState = {\n tree: { root },\n nodeCount,\n canvasSourceMap,\n previousVisibleSet: new Set(),\n currentVisibleSet: new Set(),\n };\n \n // Sync styles in the same pass if timeMs is provided\n if (timeMs !== undefined && root) {\n syncStylesWithIndex(syncState, timeMs);\n }\n \n return {\n container,\n syncState,\n };\n}\n\n/**\n * Sync a single node's styles (extracted for reuse).\n * Now uses unified style syncing with clear separation of concerns:\n * 1. Canvas pixel refresh (if canvas clone)\n * 2. Unified CSS property sync (all elements)\n * 3. Content sync (text, input values)\n */\nfunction syncNodeStyles(node: CloneNode): void {\n const { source, clone, isCanvasClone } = node;\n \n // 1. Canvas-specific: Refresh pixel content from shadow DOM\n if (isCanvasClone) {\n refreshCanvasPixels(node);\n }\n \n // 2. Unified: Sync ALL CSS properties using SYNC_PROPERTIES array\n // For canvas clones, pass content source (shadow canvas/img) for width/height\n const contentSource = isCanvasClone\n ? (source.shadowRoot?.querySelector(\"canvas\") || source.shadowRoot?.querySelector(\"img\") || undefined)\n : undefined;\n syncElementStyles(source, clone, contentSource);\n \n // 3. Element-specific: Sync text content and input values\n syncTextContent(source, clone);\n syncInputValue(source, clone);\n}\n\n// Performance instrumentation counters\ninterface SyncStats {\n nodesVisited: number;\n nodesCulledByParent: number;\n nodesCulledByTemporal: number;\n nodesProcessed: number;\n nodesFullSync: number; // Newly visible nodes (full sync)\n nodesIncrementalSync: number; // Still visible nodes (incremental sync)\n nodesHidden: number; // Newly hidden nodes\n indexQueryTimeMs: number;\n syncTimeMs: number;\n}\n\nlet syncStats: SyncStats = {\n nodesVisited: 0,\n nodesCulledByParent: 0,\n nodesCulledByTemporal: 0,\n nodesProcessed: 0,\n nodesFullSync: 0,\n nodesIncrementalSync: 0,\n nodesHidden: 0,\n indexQueryTimeMs: 0,\n syncTimeMs: 0,\n};\n\n/**\n * Visibility delta between frames.\n * Used for incremental updates - only sync what changed.\n */\ninterface VisibilityDelta {\n nowVisible: Set<CloneNode>; // Need full style sync + show\n stillVisible: Set<CloneNode>; // Only sync animated properties (or skip if same time)\n nowHidden: Set<CloneNode>; // Just set display:none\n}\n\n/**\n * Compute visibility delta between previous and current frame.\n */\nfunction computeVisibilityDelta(\n previousSet: Set<CloneNode>,\n currentSet: Set<CloneNode>,\n): VisibilityDelta {\n const nowVisible = new Set<CloneNode>();\n const stillVisible = new Set<CloneNode>();\n const nowHidden = new Set<CloneNode>();\n \n // Find nodes that became visible or stayed visible\n for (const node of currentSet) {\n if (previousSet.has(node)) {\n stillVisible.add(node);\n } else {\n nowVisible.add(node);\n }\n }\n \n // Find nodes that became hidden\n for (const node of previousSet) {\n if (!currentSet.has(node)) {\n nowHidden.add(node);\n }\n }\n \n return { nowVisible, stillVisible, nowHidden };\n}\n\n/**\n * Build visible set by recursive traversal with bounds checking.\n * Queries fresh bounds from source elements each time - bounds are computed\n * dynamically by timegroups based on composition mode.\n */\nfunction buildVisibleSetRecursive(\n node: CloneNode,\n timeMs: number,\n visibleSet: Set<CloneNode>,\n): void {\n const { children, source } = node;\n \n // Get fresh bounds from source element (not cached - timegroup bounds are dynamic)\n const bounds = getTemporalBounds(source);\n \n // Check if this node is visible at current time\n const isVisible = timeMs >= bounds.startMs && timeMs <= bounds.endMs;\n \n if (isVisible) {\n visibleSet.add(node);\n // Recurse to children\n for (const child of children) {\n buildVisibleSetRecursive(child, timeMs, visibleSet);\n }\n }\n // If not visible, skip entire subtree\n}\n\n/**\n * Sync styles with recursive visibility check and delta tracking.\n * \n * DELTA TRACKING: Tracks visibility changes between frames to minimize work:\n * - nowVisible nodes: Full style sync + show\n * - stillVisible nodes: Incremental sync (source DOM may have changed)\n * - nowHidden nodes: Just hide (display:none)\n */\nfunction syncStylesWithIndex(state: SyncState, timeMs: number): void {\n const queryStart = performance.now();\n \n // Build the set of visible nodes by recursive traversal\n const visibleSet = new Set<CloneNode>();\n if (state.tree.root) {\n buildVisibleSetRecursive(state.tree.root, timeMs, visibleSet);\n }\n \n // Compute delta from previous frame\n const delta = computeVisibilityDelta(state.previousVisibleSet, visibleSet);\n \n syncStats.indexQueryTimeMs = performance.now() - queryStart;\n \n // Now traverse the tree but use the delta for O(1) sync decisions\n const syncStart = performance.now();\n if (state.tree.root) {\n syncNodeWithDelta(state.tree.root, visibleSet, delta);\n }\n syncStats.syncTimeMs = performance.now() - syncStart;\n \n // Update state for next frame and expose current visible set\n state.previousVisibleSet = visibleSet;\n state.currentVisibleSet = visibleSet;\n}\n\n/**\n * Sync a node using visibility delta for incremental updates.\n * \n * DELTA TRACKING optimization:\n * - nowVisible: Full style sync (element just appeared)\n * - stillVisible: Incremental sync (source DOM may have changed)\n * - nowHidden: Just hide the element\n * - Not in any set: Skip entirely (was already hidden)\n */\nfunction syncNodeWithDelta(\n node: CloneNode,\n visibleSet: Set<CloneNode>,\n delta: VisibilityDelta,\n): void {\n syncStats.nodesVisited++;\n \n const isVisible = visibleSet.has(node);\n \n if (!isVisible) {\n // Node is not visible - ALWAYS set display:none\n // This handles both \"just became hidden\" and \"initial build with node outside time range\"\n node.clone.style.display = \"none\";\n if (delta.nowHidden.has(node)) {\n syncStats.nodesHidden++;\n }\n // Already hidden nodes: skip (don't even recurse to children)\n syncStats.nodesCulledByTemporal++;\n return;\n }\n \n // Node is visible - determine sync strategy\n if (delta.nowVisible.has(node)) {\n // Just became visible - need full style sync\n syncNodeStyles(node);\n syncStats.nodesFullSync++;\n } else if (delta.stillVisible.has(node)) {\n // Was visible, still visible - still need to sync\n // Source DOM properties can change independently of time (input values, text, etc.)\n // TODO: Phase 5 could track property changes for smarter incremental sync\n syncNodeStyles(node);\n syncStats.nodesIncrementalSync++;\n }\n \n syncStats.nodesProcessed++;\n \n // Recurse to children\n for (const child of node.children) {\n syncNodeWithDelta(child, visibleSet, delta);\n }\n}\n\n/**\n * Legacy recursive sync (kept for comparison/fallback).\n * Returns early if the node is temporally culled, skipping ALL descendants.\n * @deprecated Use syncStylesWithIndex for better performance\n */\nexport function syncNodeRecursiveLegacy(node: CloneNode, timeMs: number): void {\n const { clone, children, bounds } = node;\n syncStats.nodesVisited++;\n \n // Temporal culling - check if this node is visible at current time\n // NOTE: Canvas clones now participate in temporal culling (lazy canvas copying).\n // Invalid bounds [0,0] are treated as [-Infinity, Infinity] by getTemporalBounds.\n {\n // OPTIMIZATION: Check if parent is already hidden to skip bounds computation\n const parent = clone.parentElement;\n if (parent instanceof HTMLElement) {\n // If parent has display:none, this element is already hidden - skip bounds check\n if (parent.style.display === \"none\") {\n clone.style.display = \"none\";\n syncStats.nodesCulledByParent++;\n return;\n }\n }\n \n // Use cached bounds from node instead of calling getTemporalBounds\n const { startMs, endMs } = bounds;\n if (timeMs < startMs || timeMs > endMs) {\n // Hide this element and BAIL OUT - skip all descendants automatically!\n clone.style.display = \"none\";\n syncStats.nodesCulledByTemporal++;\n return;\n }\n }\n \n // Sync this node's styles\n syncNodeStyles(node);\n syncStats.nodesProcessed++;\n \n // Recursively sync children\n for (const child of children) {\n syncNodeRecursiveLegacy(child, timeMs);\n }\n}\n\n/**\n * Sync all CSS properties from source elements to their clones.\n * Uses interval index for O(log n + k) visibility queries instead of O(n) traversal.\n * Uses delta tracking for incremental updates between frames.\n */\nexport function syncStyles(state: SyncState, timeMs: number): void {\n // Reset stats\n syncStats = {\n nodesVisited: 0,\n nodesCulledByParent: 0,\n nodesCulledByTemporal: 0,\n nodesProcessed: 0,\n nodesFullSync: 0,\n nodesIncrementalSync: 0,\n nodesHidden: 0,\n indexQueryTimeMs: 0,\n syncTimeMs: 0,\n };\n \n // Use interval-index-based sync with delta tracking\n syncStylesWithIndex(state, timeMs);\n}\n\n/**\n * Collect document styles for shadow DOM injection.\n */\nexport function collectDocumentStyles(): string {\n const rules: string[] = [];\n try {\n for (const sheet of document.styleSheets) {\n try {\n if (sheet.cssRules) {\n for (const rule of sheet.cssRules) {\n rules.push(rule.cssText);\n }\n }\n } catch {}\n }\n } catch {}\n return rules.join(\"\\n\");\n}\n\n\n// Backward-compatible aliases\nexport const syncStaticStyles = syncStyles;\nexport const syncAnimatedStyles = syncStyles;\n\n/**\n * Override clip-path, opacity, and optionally transform on the root clone element.\n * The source may have these properties set for proxy mode or workbench scaling.\n * \n * @param syncState - The sync state containing the clone tree\n * @param fullReset - If true, also resets opacity and transform (for capture operations)\n */\nexport function overrideRootCloneStyles(syncState: SyncState, fullReset: boolean = false): void {\n const rootClone = syncState.tree.root?.clone;\n if (!rootClone) return;\n \n rootClone.style.clipPath = \"none\";\n if (fullReset) {\n rootClone.style.opacity = \"1\";\n rootClone.style.transform = \"none\";\n }\n}\n\n/**\n * Create a live preview of a timegroup with a refresh function.\n * Used by EFWorkbench for the \"computed\" preview mode.\n * \n * @param source - The source timegroup to preview\n * @returns Object with preview container and refresh function\n */\nexport function renderTimegroupPreview(source: Element): {\n container: HTMLDivElement;\n refresh: (timeMs?: number) => void;\n} {\n const { container, syncState } = buildCloneStructure(source);\n \n // Initial style sync\n syncStyles(syncState, 0);\n \n return {\n container,\n refresh: (timeMs?: number) => {\n syncStyles(syncState, timeMs ?? 0);\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"renderTimegroupPreview.js","names":["CSS_SAFE_DEFAULT_VALUES: Record<string, string | string[]>","removed: RemovedNodeInfo[]","cs: CSSStyleDeclaration","contentCs: CSSStyleDeclaration | undefined","srcMap: StylePropertyMapReadOnly","node: CloneNode","node","clone","syncState: SyncState","syncStats: SyncStats","rules: string[]"],"sources":["../../src/preview/renderTimegroupPreview.ts"],"sourcesContent":["/**\n * Canvas refresh fix:\n * Canvas pixels must be explicitly cleared and redrawn in syncNodeStyles\n * to ensure video frames are captured correctly during foreignObject rendering.\n * Without clearRect, stale frames from previous seeks may be serialized.\n * \n * See FOREIGNOBJECT_BUG_FIX.md for detailed explanation.\n */\n\nimport { logger } from \"./logger.js\";\n\n/**\n * Elements to skip entirely when building the preview.\n */\nconst SKIP_TAGS = new Set([\n \"EF-AUDIO\",\n \"EF-THUMBNAIL-STRIP\",\n \"EF-FILMSTRIP\",\n \"EF-TIMELINE\",\n \"EF-WORKBENCH\",\n \"SCRIPT\",\n \"STYLE\",\n]);\n\n/**\n * All CSS properties to sync (camelCase for style[] access).\n */\nconst SYNC_PROPERTIES = [\n \"display\", \"visibility\", \"opacity\",\n \"position\", \"top\", \"right\", \"bottom\", \"left\", \"zIndex\",\n \"width\", \"height\", \"minWidth\", \"minHeight\", \"maxWidth\", \"maxHeight\",\n \"flex\", \"flexFlow\", \"justifyContent\", \"alignItems\", \"alignContent\", \"alignSelf\", \"gap\",\n \"gridTemplate\", \"gridColumn\", \"gridRow\", \"gridArea\",\n \"margin\", \"padding\", \"boxSizing\",\n \"border\", \"borderTop\", \"borderRight\", \"borderBottom\", \"borderLeft\", \"borderRadius\",\n \"background\", \"color\", \"boxShadow\", \"filter\", \"backdropFilter\", \"clipPath\",\n \"font\", \"textAlign\", \"textDecoration\", \"textTransform\",\n \"letterSpacing\", \"whiteSpace\", \"textOverflow\", \"lineHeight\",\n \"transform\", \"transformOrigin\", \"transformStyle\",\n \"perspective\", \"perspectiveOrigin\", \"backfaceVisibility\",\n \"cursor\", \"pointerEvents\", \"userSelect\", \"overflow\",\n] as const;\n\n/**\n * Kebab-case versions for computedStyleMap.get() - pre-computed for speed.\n */\nconst SYNC_PROPERTIES_KEBAB = SYNC_PROPERTIES.map(prop =>\n prop.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`)\n);\n\n/**\n * Feature detection: computedStyleMap is ~15% faster for style syncing.\n */\nconst HAS_COMPUTED_STYLE_MAP = typeof Element !== \"undefined\" && typeof Element.prototype.computedStyleMap === \"function\";\n\n/**\n * CSS initial/default values for SAFE-TO-SKIP properties.\n * Only includes NON-INHERITED properties where skipping the default\n * won't affect visual output.\n * \n * EXCLUDED (must always serialize):\n * - Inherited properties (color, font, text-*, visibility, cursor)\n * - Display (affects layout significantly)\n * - Properties where \"auto\" computes to a specific value\n * \n * INCLUDED (safe to skip):\n * - Transform/filter effects (none = no effect)\n * - Box shadows (none = no shadow)\n * - Borders when none/0 (no visual impact)\n * - backdrop-filter (none = no effect)\n */\nconst CSS_SAFE_DEFAULT_VALUES: Record<string, string | string[]> = {\n // Transforms & effects - safe to skip \"none\" (no visual impact)\n transform: \"none\",\n filter: \"none\",\n backdropFilter: \"none\",\n boxShadow: \"none\",\n \n // Borders - safe to skip when none/0\n border: [\"none\", \"0px none\", \"0px\", \"0px none rgb(0, 0, 0)\"],\n borderTop: [\"none\", \"0px none\", \"0px\", \"0px none rgb(0, 0, 0)\"],\n borderRight: [\"none\", \"0px none\", \"0px\", \"0px none rgb(0, 0, 0)\"],\n borderBottom: [\"none\", \"0px none\", \"0px\", \"0px none rgb(0, 0, 0)\"],\n borderLeft: [\"none\", \"0px none\", \"0px\", \"0px none rgb(0, 0, 0)\"],\n borderRadius: [\"0px\", \"0\"],\n \n // Positioning - safe to skip \"static\"\n position: \"static\",\n \n // Z-index - \"auto\" is safe when position is static\n zIndex: \"auto\",\n \n // 3D transforms - safe to skip defaults\n transformStyle: \"flat\",\n perspective: \"none\",\n backfaceVisibility: \"visible\",\n};\n\n/**\n * Check if a value matches a safe-to-skip default.\n */\nfunction isDefaultValue(prop: string, value: string): boolean {\n const defaults = CSS_SAFE_DEFAULT_VALUES[prop];\n if (!defaults) return false;\n if (Array.isArray(defaults)) {\n return defaults.includes(value);\n }\n return defaults === value;\n}\n\n// Re-export temporal types from shared module\nexport {\n type TemporalElement,\n isTemporal,\n getTemporalBounds,\n isVisibleAtTime,\n} from \"./previewTypes.js\";\n\n// Import for internal use\nimport {\n getTemporalBounds,\n} from \"./previewTypes.js\";\n\n/**\n * Tree node representing a source/clone pair with children.\n * This replaces the flat array approach for cleaner recursive traversal.\n */\nexport interface CloneNode {\n source: Element;\n clone: HTMLElement;\n children: CloneNode[];\n isCanvasClone: boolean;\n /** Cached temporal bounds for this node */\n bounds: { startMs: number; endMs: number };\n /** Parent node reference for ancestor visibility checks */\n parent: CloneNode | null;\n}\n\n/** Tree-based sync state */\nexport interface CloneTree {\n root: CloneNode | null;\n}\n\n/** Sync state with tree structure and delta tracking */\nexport interface SyncState {\n tree: CloneTree;\n nodeCount: number; // Total number of nodes (for debugging/logging)\n /** Maps clone canvases to their original source elements (ef-video, ef-image, etc.) */\n canvasSourceMap: WeakMap<HTMLCanvasElement, Element>;\n /** Previous frame's visible set for delta tracking */\n previousVisibleSet: Set<CloneNode>;\n /** Current frame's visible set (updated by syncStyles) */\n currentVisibleSet: Set<CloneNode>;\n}\n\n/** Info needed to restore a removed node */\ninterface RemovedNodeInfo {\n node: CloneNode;\n parent: Node;\n nextSibling: Node | null;\n}\n\n/**\n * Remove hidden nodes from the clone DOM for serialization.\n * Returns info needed to restore them afterward.\n * \n * This physically removes non-visible nodes so they won't be serialized,\n * avoiding the cost of serializing hidden elements and their resources.\n */\nexport function removeHiddenNodesForSerialization(state: SyncState): RemovedNodeInfo[] {\n const removed: RemovedNodeInfo[] = [];\n const visibleSet = state.currentVisibleSet;\n \n // Traverse all nodes and remove those not in visible set\n function visit(node: CloneNode): void {\n // First recurse to children (before potentially removing this node)\n for (const child of node.children) {\n visit(child);\n }\n \n // If this node isn't visible, remove it from DOM\n if (!visibleSet.has(node)) {\n const parent = node.clone.parentNode;\n if (parent) {\n const nextSibling = node.clone.nextSibling;\n parent.removeChild(node.clone);\n removed.push({ node, parent, nextSibling });\n }\n }\n }\n \n if (state.tree.root) {\n visit(state.tree.root);\n }\n \n return removed;\n}\n\n/**\n * Restore previously removed hidden nodes to the clone DOM.\n * Must be called after serialization to maintain tree integrity for next frame.\n */\nexport function restoreHiddenNodes(removed: RemovedNodeInfo[]): void {\n // Restore in reverse order to maintain correct DOM positions\n for (let i = removed.length - 1; i >= 0; i--) {\n const { node, parent, nextSibling } = removed[i]!;\n if (nextSibling) {\n parent.insertBefore(node.clone, nextSibling);\n } else {\n parent.appendChild(node.clone);\n }\n }\n}\n\n/**\n * Get visible canvases from the current visible set.\n * Use this to skip encoding hidden canvases during serialization.\n */\nexport function getVisibleCanvases(state: SyncState): Set<HTMLCanvasElement> {\n const visibleCanvases = new Set<HTMLCanvasElement>();\n for (const node of state.currentVisibleSet) {\n if (node.clone instanceof HTMLCanvasElement) {\n visibleCanvases.add(node.clone);\n }\n }\n return visibleCanvases;\n}\n\n/**\n * Traverse all nodes in the clone tree, calling the callback for each.\n */\nexport function traverseCloneTree(state: SyncState, callback: (node: CloneNode) => void): void {\n function visit(node: CloneNode): void {\n callback(node);\n for (const child of node.children) {\n visit(child);\n }\n }\n if (state.tree.root) {\n visit(state.tree.root);\n }\n}\n\n/**\n * Unified CSS property sync for all elements (canvas clones and regular elements).\n * \n * Canvas clones use a limited property set matching the original implementation\n * to avoid dimension/layout issues. Regular elements use the full SYNC_PROPERTIES array.\n * \n * @param source - Source element to read styles from\n * @param clone - Clone element to write styles to\n * @param contentSource - Optional content element for width/height (canvas clones only)\n */\nfunction syncElementStyles(\n source: Element,\n clone: HTMLElement,\n contentSource?: Element,\n): void {\n const cloneStyle = clone.style as any;\n const tagName = (source as HTMLElement).tagName;\n const isCanvasClone = !!contentSource;\n \n // Canvas clones: Use exact property list from original implementation\n if (isCanvasClone) {\n let cs: CSSStyleDeclaration;\n let contentCs: CSSStyleDeclaration | undefined;\n \n try {\n cs = getComputedStyle(source);\n if (contentSource) {\n contentCs = getComputedStyle(contentSource);\n }\n } catch { return; }\n \n // Exact properties from original copyCanvasCloneStyles + syncNodeStyles\n cloneStyle.position = cs.position;\n cloneStyle.top = cs.top;\n cloneStyle.right = cs.right;\n cloneStyle.bottom = cs.bottom;\n cloneStyle.left = cs.left;\n cloneStyle.margin = cs.margin;\n cloneStyle.zIndex = cs.zIndex;\n cloneStyle.transform = cs.transform;\n cloneStyle.transformOrigin = cs.transformOrigin;\n cloneStyle.opacity = cs.opacity;\n cloneStyle.visibility = cs.visibility;\n cloneStyle.backfaceVisibility = cs.backfaceVisibility;\n cloneStyle.transformStyle = cs.transformStyle;\n \n // Width/height from content source (shadow canvas/img)\n if (contentCs) {\n cloneStyle.width = contentCs.width;\n cloneStyle.height = contentCs.height;\n }\n \n cloneStyle.display = \"block\";\n cloneStyle.animation = \"none\";\n cloneStyle.transition = \"none\";\n \n return;\n }\n \n // Regular elements: full property sync from SYNC_PROPERTIES\n const propLen = SYNC_PROPERTIES.length;\n \n if (HAS_COMPUTED_STYLE_MAP) {\n let srcMap: StylePropertyMapReadOnly;\n \n try {\n srcMap = source.computedStyleMap();\n } catch { return; }\n \n for (let j = 0; j < propLen; j++) {\n const kebab = SYNC_PROPERTIES_KEBAB[j]!;\n const camel = SYNC_PROPERTIES[j]!;\n \n const srcVal = srcMap.get(kebab);\n if (!srcVal) continue;\n \n const strVal = srcVal.toString();\n \n if (camel === \"display\") {\n // For caption child elements, preserve display:none when explicitly set\n // (they use it to hide empty content, not for temporal visibility)\n const isCaptionChild = tagName && (\n tagName === 'EF-CAPTIONS-ACTIVE-WORD' ||\n tagName === 'EF-CAPTIONS-BEFORE-ACTIVE-WORD' ||\n tagName === 'EF-CAPTIONS-AFTER-ACTIVE-WORD' ||\n tagName === 'EF-CAPTIONS-SEGMENT'\n );\n const targetDisplay = (strVal === \"none\" && !isCaptionChild) ? \"block\" : strVal;\n cloneStyle.display = targetDisplay;\n continue;\n }\n \n // Skip clipPath - clones always have clipPath: none for rendering\n // (source may have clip-path: inset(100%) from proxy mode)\n if (camel === \"clipPath\") continue;\n \n // OPTIMIZATION: Skip default values to reduce serialized HTML size\n // If the computed value is the CSS default, don't set it as inline style\n if (isDefaultValue(camel, strVal)) {\n // Remove from inline style if it was previously set\n if (cloneStyle[camel]) cloneStyle[camel] = \"\";\n continue;\n }\n \n cloneStyle[camel] = strVal;\n }\n } else {\n let cs: CSSStyleDeclaration;\n \n try {\n cs = getComputedStyle(source);\n } catch { return; }\n \n const srcStyle = cs as any;\n \n for (const prop of SYNC_PROPERTIES) {\n const srcVal = srcStyle[prop];\n if (!srcVal) continue;\n \n if (prop === \"display\") {\n // For caption child elements, preserve display:none when explicitly set\n // (they use it to hide empty content, not for temporal visibility)\n const isCaptionChild = tagName && (\n tagName === 'EF-CAPTIONS-ACTIVE-WORD' ||\n tagName === 'EF-CAPTIONS-BEFORE-ACTIVE-WORD' ||\n tagName === 'EF-CAPTIONS-AFTER-ACTIVE-WORD' ||\n tagName === 'EF-CAPTIONS-SEGMENT'\n );\n const targetDisplay = (srcVal === \"none\" && !isCaptionChild) ? \"block\" : srcVal;\n cloneStyle.display = targetDisplay;\n continue;\n }\n \n // Skip clipPath - clones always have clipPath: none for rendering\n // (source may have clip-path: inset(100%) from proxy mode)\n if (prop === \"clipPath\") continue;\n \n // OPTIMIZATION: Skip default values to reduce serialized HTML size\n if (isDefaultValue(prop, srcVal)) {\n if (cloneStyle[prop]) cloneStyle[prop] = \"\";\n continue;\n }\n \n cloneStyle[prop] = srcVal;\n }\n }\n \n // Disable animations/transitions to prevent re-animation\n cloneStyle.animation = \"none\";\n cloneStyle.transition = \"none\";\n}\n\n/**\n * Refresh canvas pixel content from shadow DOM source.\n * Handles both shadow canvas and shadow img sources.\n */\nfunction refreshCanvasPixels(node: CloneNode): void {\n const { source, clone } = node;\n const canvas = clone as HTMLCanvasElement;\n const shadowCanvas = source.shadowRoot?.querySelector(\"canvas\");\n const shadowImg = source.shadowRoot?.querySelector(\"img\");\n \n if (shadowCanvas) {\n // Update buffer dimensions if needed\n if (canvas.width !== shadowCanvas.width) canvas.width = shadowCanvas.width;\n if (canvas.height !== shadowCanvas.height) canvas.height = shadowCanvas.height;\n \n // Copy pixels with explicit clear\n const ctx = canvas.getContext(\"2d\");\n if (ctx && shadowCanvas.width > 0 && shadowCanvas.height > 0) {\n try {\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n ctx.drawImage(shadowCanvas, 0, 0);\n } catch (e) {\n logger.warn(\"[refreshCanvasPixels] Canvas draw failed:\", e);\n }\n }\n } else if (shadowImg?.complete && shadowImg.naturalWidth > 0) {\n // Update buffer dimensions if needed\n if (canvas.width !== shadowImg.naturalWidth) canvas.width = shadowImg.naturalWidth;\n if (canvas.height !== shadowImg.naturalHeight) canvas.height = shadowImg.naturalHeight;\n \n // Copy pixels with explicit clear\n const ctx = canvas.getContext(\"2d\");\n if (ctx) {\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n try { ctx.drawImage(shadowImg, 0, 0); } catch {}\n }\n }\n}\n\n/**\n * Sync text content from light DOM to clone.\n */\nfunction syncTextContent(source: Element, clone: HTMLElement): void {\n const srcTextNode = source.childNodes[0];\n if (srcTextNode?.nodeType === Node.TEXT_NODE) {\n const srcText = srcTextNode.textContent || \"\";\n const cloneTextNode = clone.childNodes[0];\n \n if (cloneTextNode?.nodeType === Node.TEXT_NODE) {\n // Update existing text node\n if (cloneTextNode.textContent !== srcText) cloneTextNode.textContent = srcText;\n } else if (!clone.childNodes.length) {\n // Only create text node if clone has NO children (was empty when initially cloned)\n // Don't set textContent as it would delete element children!\n clone.appendChild(document.createTextNode(srcText));\n }\n }\n}\n\n/**\n * Sync input element value.\n */\nfunction syncInputValue(source: Element, clone: HTMLElement): void {\n if (source instanceof HTMLInputElement) {\n const srcVal = source.value;\n const cloneInput = clone as HTMLInputElement;\n if (cloneInput.value !== srcVal) {\n cloneInput.value = srcVal;\n cloneInput.setAttribute(\"value\", srcVal);\n }\n }\n}\n\n/**\n * Build clone tree structure with minimal overhead.\n * Caches temporal bounds on each node for visibility checks.\n * Optionally syncs styles in the same pass if timeMs is provided.\n */\nexport function buildCloneStructure(source: Element, timeMs?: number): {\n container: HTMLDivElement;\n syncState: SyncState;\n} {\n const container = document.createElement(\"div\");\n container.style.cssText = \"position:absolute;top:0;left:0;width:100%;height:100%\";\n \n let nodeCount = 0;\n const canvasSourceMap = new WeakMap<HTMLCanvasElement, Element>();\n \n function cloneElement(srcEl: Element, parentNode: CloneNode | null): CloneNode | null {\n if (SKIP_TAGS.has(srcEl.tagName)) return null;\n \n // Get temporal bounds upfront for indexing\n const bounds = getTemporalBounds(srcEl);\n \n // SVG - clone entire subtree (no children tracking needed)\n if (srcEl instanceof SVGElement) {\n const svgClone = srcEl.cloneNode(true) as SVGElement;\n const node: CloneNode = {\n source: srcEl,\n clone: svgClone as unknown as HTMLElement,\n children: [],\n isCanvasClone: false,\n bounds,\n parent: parentNode,\n };\n nodeCount++;\n return node;\n }\n \n // Canvas - copy pixels\n if (srcEl instanceof HTMLCanvasElement) {\n const canvas = document.createElement(\"canvas\");\n canvas.width = srcEl.width;\n canvas.height = srcEl.height;\n const ctx = canvas.getContext(\"2d\");\n if (ctx) {\n try { ctx.drawImage(srcEl, 0, 0); } catch {}\n }\n // Raw canvas elements don't need style syncing, just return clone\n // return null;\n }\n \n // Custom elements with shadow canvas (e.g., ef-video, ef-image)\n const isCustom = srcEl.tagName.includes(\"-\");\n if (isCustom && srcEl.shadowRoot) {\n const shadowCanvas = srcEl.shadowRoot.querySelector(\"canvas\");\n if (shadowCanvas) {\n const clone = document.createElement(\"canvas\");\n clone.width = shadowCanvas.width || srcEl.clientWidth;\n clone.height = shadowCanvas.height || srcEl.clientHeight;\n // Check if the element actually has alpha channel before preserving it\n // ef-image tracks hasAlpha based on MIME type (JPEG=false, PNG/WebP=true)\n // ef-waveform always needs alpha for proper rendering\n if (srcEl.tagName === \"EF-WAVEFORM\") {\n clone.dataset.preserveAlpha = \"true\";\n } else if (srcEl.tagName === \"EF-IMAGE\" && \"hasAlpha\" in srcEl && (srcEl as any).hasAlpha) {\n clone.dataset.preserveAlpha = \"true\";\n }\n \n const ctx = clone.getContext(\"2d\");\n if (ctx) {\n try { ctx.drawImage(shadowCanvas, 0, 0); } catch {}\n }\n \n // Copy initial CSS styles using unified sync\n // Pass shadowCanvas as contentSource for width/height\n try {\n syncElementStyles(srcEl, clone, shadowCanvas);\n } catch {}\n \n // Map clone canvas to source element for RenderContext caching\n canvasSourceMap.set(clone, srcEl);\n \n const node: CloneNode = {\n source: srcEl,\n clone,\n children: [],\n isCanvasClone: true,\n bounds,\n parent: parentNode,\n };\n nodeCount++;\n return node;\n }\n \n const shadowImg = srcEl.shadowRoot.querySelector(\"img\");\n if (shadowImg?.complete && shadowImg.naturalWidth > 0) {\n const clone = document.createElement(\"canvas\");\n clone.width = shadowImg.naturalWidth;\n clone.height = shadowImg.naturalHeight;\n // Check if the element actually has alpha channel before preserving it\n // For direct img elements, check the element's hasAlpha property\n if (srcEl.tagName === \"EF-IMAGE\" && \"hasAlpha\" in srcEl && (srcEl as any).hasAlpha) {\n clone.dataset.preserveAlpha = \"true\";\n }\n const ctx = clone.getContext(\"2d\");\n if (ctx) {\n try { ctx.drawImage(shadowImg, 0, 0); } catch {}\n }\n \n // Copy initial CSS styles using unified sync\n // Pass shadowImg as contentSource for width/height\n try {\n syncElementStyles(srcEl, clone, shadowImg);\n } catch {}\n \n // Map clone canvas to source element for RenderContext caching\n canvasSourceMap.set(clone, srcEl);\n \n const node: CloneNode = {\n source: srcEl,\n clone,\n children: [],\n isCanvasClone: true,\n bounds,\n parent: parentNode,\n };\n nodeCount++;\n return node;\n }\n }\n \n // Standard element clone\n const clone = document.createElement(isCustom ? \"div\" : srcEl.tagName.toLowerCase()) as HTMLElement;\n \n // Copy attributes - OPTIMIZATION: Early exit if no attributes\n const attrs = srcEl.attributes;\n const attrLen = attrs.length;\n if (attrLen > 0) {\n for (let i = 0; i < attrLen; i++) {\n const attr = attrs[i]!;\n const name = attr.name.toLowerCase();\n if (name === \"id\" || name.startsWith(\"on\")) continue;\n if (isCustom && name !== \"class\" && !name.startsWith(\"data-\")) continue;\n try { clone.setAttribute(attr.name, attr.value); } catch {}\n }\n }\n \n if (srcEl instanceof HTMLImageElement && srcEl.src) {\n (clone as HTMLImageElement).src = srcEl.src;\n }\n if (srcEl instanceof HTMLInputElement) {\n (clone as HTMLInputElement).value = srcEl.value;\n }\n \n const node: CloneNode = {\n source: srcEl,\n clone,\n children: [],\n isCanvasClone: false,\n bounds,\n parent: parentNode,\n };\n nodeCount++;\n \n // Shadow DOM children - OPTIMIZATION: Early exit if no childNodes\n if (srcEl.shadowRoot) {\n const shadowChildren = srcEl.shadowRoot.childNodes;\n const shadowLen = shadowChildren.length;\n if (shadowLen > 0) {\n // For text segments, ALWAYS create a text node placeholder even if empty.\n // Caption elements now use light DOM, so they don't need special handling here.\n const isTextSegment = srcEl.tagName === 'EF-TEXT-SEGMENT';\n let hasTextNode = false;\n \n for (let i = 0; i < shadowLen; i++) {\n const child = shadowChildren[i]!;\n if (child.nodeType === Node.TEXT_NODE) {\n const text = child.textContent?.trim();\n // Always include text for text segments (even if whitespace-only, e.g., \" \")\n if (text || isTextSegment) {\n clone.appendChild(document.createTextNode(child.textContent || \"\"));\n hasTextNode = true;\n }\n } else if (child.nodeType === Node.ELEMENT_NODE) {\n const el = child as Element;\n if (el.tagName === \"STYLE\" || el.tagName === \"SLOT\") continue;\n const childNode = cloneElement(el, node);\n if (childNode) {\n node.children.push(childNode);\n clone.appendChild(childNode.clone);\n }\n }\n }\n \n // For text segments, ensure there's always a text node for syncStyles to update\n if (isTextSegment && !hasTextNode) {\n clone.appendChild(document.createTextNode(\"\"));\n }\n }\n }\n \n // Light DOM children - OPTIMIZATION: Use indexed loop for performance\n const lightChildren = srcEl.childNodes;\n const lightLen = lightChildren.length;\n for (let i = 0; i < lightLen; i++) {\n const child = lightChildren[i]!;\n if (child.nodeType === Node.TEXT_NODE) {\n const text = child.textContent?.trim();\n if (text) clone.appendChild(document.createTextNode(text));\n } else if (child.nodeType === Node.ELEMENT_NODE) {\n const childNode = cloneElement(child as Element, node);\n if (childNode) {\n node.children.push(childNode);\n clone.appendChild(childNode.clone);\n }\n }\n }\n \n return node;\n }\n \n const root = cloneElement(source, null);\n if (root) container.appendChild(root.clone);\n \n const syncState: SyncState = {\n tree: { root },\n nodeCount,\n canvasSourceMap,\n previousVisibleSet: new Set(),\n currentVisibleSet: new Set(),\n };\n \n // Sync styles in the same pass if timeMs is provided\n if (timeMs !== undefined && root) {\n syncStylesWithIndex(syncState, timeMs);\n }\n \n return {\n container,\n syncState,\n };\n}\n\n/**\n * Sync a single node's styles (extracted for reuse).\n * Now uses unified style syncing with clear separation of concerns:\n * 1. Canvas pixel refresh (if canvas clone)\n * 2. Unified CSS property sync (all elements)\n * 3. Content sync (text, input values)\n */\nfunction syncNodeStyles(node: CloneNode): void {\n const { source, clone, isCanvasClone } = node;\n \n // 1. Canvas-specific: Refresh pixel content from shadow DOM\n if (isCanvasClone) {\n refreshCanvasPixels(node);\n }\n \n // 2. Unified: Sync ALL CSS properties using SYNC_PROPERTIES array\n // For canvas clones, pass content source (shadow canvas/img) for width/height\n const contentSource = isCanvasClone\n ? (source.shadowRoot?.querySelector(\"canvas\") || source.shadowRoot?.querySelector(\"img\") || undefined)\n : undefined;\n syncElementStyles(source, clone, contentSource);\n \n // 3. Element-specific: Sync text content and input values\n syncTextContent(source, clone);\n syncInputValue(source, clone);\n}\n\n// Performance instrumentation counters\ninterface SyncStats {\n nodesVisited: number;\n nodesCulledByParent: number;\n nodesCulledByTemporal: number;\n nodesProcessed: number;\n nodesFullSync: number; // Newly visible nodes (full sync)\n nodesIncrementalSync: number; // Still visible nodes (incremental sync)\n nodesHidden: number; // Newly hidden nodes\n indexQueryTimeMs: number;\n syncTimeMs: number;\n}\n\nlet syncStats: SyncStats = {\n nodesVisited: 0,\n nodesCulledByParent: 0,\n nodesCulledByTemporal: 0,\n nodesProcessed: 0,\n nodesFullSync: 0,\n nodesIncrementalSync: 0,\n nodesHidden: 0,\n indexQueryTimeMs: 0,\n syncTimeMs: 0,\n};\n\n/**\n * Visibility delta between frames.\n * Used for incremental updates - only sync what changed.\n */\ninterface VisibilityDelta {\n nowVisible: Set<CloneNode>; // Need full style sync + show\n stillVisible: Set<CloneNode>; // Only sync animated properties (or skip if same time)\n nowHidden: Set<CloneNode>; // Just set display:none\n}\n\n/**\n * Compute visibility delta between previous and current frame.\n */\nfunction computeVisibilityDelta(\n previousSet: Set<CloneNode>,\n currentSet: Set<CloneNode>,\n): VisibilityDelta {\n const nowVisible = new Set<CloneNode>();\n const stillVisible = new Set<CloneNode>();\n const nowHidden = new Set<CloneNode>();\n \n // Find nodes that became visible or stayed visible\n for (const node of currentSet) {\n if (previousSet.has(node)) {\n stillVisible.add(node);\n } else {\n nowVisible.add(node);\n }\n }\n \n // Find nodes that became hidden\n for (const node of previousSet) {\n if (!currentSet.has(node)) {\n nowHidden.add(node);\n }\n }\n \n return { nowVisible, stillVisible, nowHidden };\n}\n\n/**\n * Build visible set by recursive traversal with bounds checking.\n * Queries fresh bounds from source elements each time - bounds are computed\n * dynamically by timegroups based on composition mode.\n */\nfunction buildVisibleSetRecursive(\n node: CloneNode,\n timeMs: number,\n visibleSet: Set<CloneNode>,\n): void {\n const { children, source } = node;\n \n // Get fresh bounds from source element (not cached - timegroup bounds are dynamic)\n const bounds = getTemporalBounds(source);\n \n // Check if this node is visible at current time\n const isVisible = timeMs >= bounds.startMs && timeMs <= bounds.endMs;\n \n if (isVisible) {\n visibleSet.add(node);\n // Recurse to children\n for (const child of children) {\n buildVisibleSetRecursive(child, timeMs, visibleSet);\n }\n }\n // If not visible, skip entire subtree\n}\n\n/**\n * Sync styles with recursive visibility check and delta tracking.\n * \n * DELTA TRACKING: Tracks visibility changes between frames to minimize work:\n * - nowVisible nodes: Full style sync + show\n * - stillVisible nodes: Incremental sync (source DOM may have changed)\n * - nowHidden nodes: Just hide (display:none)\n */\nfunction syncStylesWithIndex(state: SyncState, timeMs: number): void {\n const queryStart = performance.now();\n \n // Build the set of visible nodes by recursive traversal\n const visibleSet = new Set<CloneNode>();\n if (state.tree.root) {\n buildVisibleSetRecursive(state.tree.root, timeMs, visibleSet);\n }\n \n // Compute delta from previous frame\n const delta = computeVisibilityDelta(state.previousVisibleSet, visibleSet);\n \n syncStats.indexQueryTimeMs = performance.now() - queryStart;\n \n // Now traverse the tree but use the delta for O(1) sync decisions\n const syncStart = performance.now();\n if (state.tree.root) {\n syncNodeWithDelta(state.tree.root, visibleSet, delta);\n }\n syncStats.syncTimeMs = performance.now() - syncStart;\n \n // Update state for next frame and expose current visible set\n state.previousVisibleSet = visibleSet;\n state.currentVisibleSet = visibleSet;\n}\n\n/**\n * Sync a node using visibility delta for incremental updates.\n * \n * DELTA TRACKING optimization:\n * - nowVisible: Full style sync (element just appeared)\n * - stillVisible: Incremental sync (source DOM may have changed)\n * - nowHidden: Just hide the element\n * - Not in any set: Skip entirely (was already hidden)\n */\nfunction syncNodeWithDelta(\n node: CloneNode,\n visibleSet: Set<CloneNode>,\n delta: VisibilityDelta,\n): void {\n syncStats.nodesVisited++;\n \n const isVisible = visibleSet.has(node);\n \n if (!isVisible) {\n // Node is not visible - ALWAYS set display:none\n // This handles both \"just became hidden\" and \"initial build with node outside time range\"\n node.clone.style.display = \"none\";\n if (delta.nowHidden.has(node)) {\n syncStats.nodesHidden++;\n }\n // Already hidden nodes: skip (don't even recurse to children)\n syncStats.nodesCulledByTemporal++;\n return;\n }\n \n // Node is visible - determine sync strategy\n if (delta.nowVisible.has(node)) {\n // Just became visible - need full style sync\n syncNodeStyles(node);\n syncStats.nodesFullSync++;\n } else if (delta.stillVisible.has(node)) {\n // Was visible, still visible - still need to sync\n // Source DOM properties can change independently of time (input values, text, etc.)\n // TODO: Phase 5 could track property changes for smarter incremental sync\n syncNodeStyles(node);\n syncStats.nodesIncrementalSync++;\n }\n \n syncStats.nodesProcessed++;\n \n // Recurse to children\n for (const child of node.children) {\n syncNodeWithDelta(child, visibleSet, delta);\n }\n}\n\n/**\n * Legacy recursive sync (kept for comparison/fallback).\n * Returns early if the node is temporally culled, skipping ALL descendants.\n * @deprecated Use syncStylesWithIndex for better performance\n */\nexport function syncNodeRecursiveLegacy(node: CloneNode, timeMs: number): void {\n const { clone, children, bounds } = node;\n syncStats.nodesVisited++;\n \n // Temporal culling - check if this node is visible at current time\n // NOTE: Canvas clones now participate in temporal culling (lazy canvas copying).\n // Invalid bounds [0,0] are treated as [-Infinity, Infinity] by getTemporalBounds.\n {\n // OPTIMIZATION: Check if parent is already hidden to skip bounds computation\n const parent = clone.parentElement;\n if (parent instanceof HTMLElement) {\n // If parent has display:none, this element is already hidden - skip bounds check\n if (parent.style.display === \"none\") {\n clone.style.display = \"none\";\n syncStats.nodesCulledByParent++;\n return;\n }\n }\n \n // Use cached bounds from node instead of calling getTemporalBounds\n const { startMs, endMs } = bounds;\n if (timeMs < startMs || timeMs > endMs) {\n // Hide this element and BAIL OUT - skip all descendants automatically!\n clone.style.display = \"none\";\n syncStats.nodesCulledByTemporal++;\n return;\n }\n }\n \n // Sync this node's styles\n syncNodeStyles(node);\n syncStats.nodesProcessed++;\n \n // Recursively sync children\n for (const child of children) {\n syncNodeRecursiveLegacy(child, timeMs);\n }\n}\n\n/**\n * Sync all CSS properties from source elements to their clones.\n * Uses interval index for O(log n + k) visibility queries instead of O(n) traversal.\n * Uses delta tracking for incremental updates between frames.\n */\nexport function syncStyles(state: SyncState, timeMs: number): void {\n // Reset stats\n syncStats = {\n nodesVisited: 0,\n nodesCulledByParent: 0,\n nodesCulledByTemporal: 0,\n nodesProcessed: 0,\n nodesFullSync: 0,\n nodesIncrementalSync: 0,\n nodesHidden: 0,\n indexQueryTimeMs: 0,\n syncTimeMs: 0,\n };\n \n // Use interval-index-based sync with delta tracking\n syncStylesWithIndex(state, timeMs);\n}\n\n/**\n * Collect document styles for shadow DOM injection.\n */\nexport function collectDocumentStyles(): string {\n const rules: string[] = [];\n try {\n for (const sheet of document.styleSheets) {\n try {\n if (sheet.cssRules) {\n for (const rule of sheet.cssRules) {\n rules.push(rule.cssText);\n }\n }\n } catch {}\n }\n } catch {}\n return rules.join(\"\\n\");\n}\n\n\n// Backward-compatible aliases\nexport const syncStaticStyles = syncStyles;\nexport const syncAnimatedStyles = syncStyles;\n\n/**\n * Override clip-path, opacity, and optionally transform on the root clone element.\n * The source may have these properties set for proxy mode or workbench scaling.\n * \n * @param syncState - The sync state containing the clone tree\n * @param fullReset - If true, also resets opacity and transform (for capture operations)\n */\nexport function overrideRootCloneStyles(syncState: SyncState, fullReset: boolean = false): void {\n const rootClone = syncState.tree.root?.clone;\n if (!rootClone) return;\n \n rootClone.style.clipPath = \"none\";\n if (fullReset) {\n rootClone.style.opacity = \"1\";\n rootClone.style.transform = \"none\";\n }\n}\n\n/**\n * Create a live preview of a timegroup with a refresh function.\n * Used by EFWorkbench for the \"computed\" preview mode.\n * \n * @param source - The source timegroup to preview\n * @returns Object with preview container and refresh function\n */\nexport function renderTimegroupPreview(source: Element): {\n container: HTMLDivElement;\n refresh: (timeMs?: number) => void;\n} {\n const { container, syncState } = buildCloneStructure(source);\n \n // Initial style sync\n syncStyles(syncState, 0);\n \n return {\n container,\n refresh: (timeMs?: number) => {\n syncStyles(syncState, timeMs ?? 0);\n },\n };\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@editframe/elements",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.0-beta",
|
|
4
4
|
"description": "",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"license": "UNLICENSED",
|
|
14
14
|
"dependencies": {
|
|
15
15
|
"@bramus/style-observer": "^1.3.0",
|
|
16
|
-
"@editframe/assets": "0.
|
|
16
|
+
"@editframe/assets": "0.35.0-beta",
|
|
17
17
|
"@lit/context": "^1.1.6",
|
|
18
18
|
"@opentelemetry/api": "^1.9.0",
|
|
19
19
|
"@opentelemetry/context-zone": "^1.26.0",
|