@editframe/elements 0.42.3 → 0.42.5
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/EFText.js +20 -6
- package/dist/elements/EFText.js.map +1 -1
- package/dist/elements/EFTextSegment.js +1 -0
- package/dist/elements/EFTextSegment.js.map +1 -1
- package/dist/elements/updateAnimations.js +7 -5
- package/dist/elements/updateAnimations.js.map +1 -1
- package/package.json +2 -2
package/dist/elements/EFText.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { updateAnimations } from "./updateAnimations.js";
|
|
2
2
|
import { durationConverter } from "./durationConverter.js";
|
|
3
3
|
import { __decorate } from "../_virtual/_@oxc-project_runtime@0.95.0/helpers/decorate.js";
|
|
4
|
-
import { EFTemporal } from "./EFTemporal.js";
|
|
4
|
+
import { EFTemporal, flushStartTimeMsCache } from "./EFTemporal.js";
|
|
5
5
|
import { evaluateEasing } from "./easingUtils.js";
|
|
6
|
+
import { flushSequenceDurationCache } from "./EFTimegroup.js";
|
|
6
7
|
import { LitElement, css, html } from "lit";
|
|
7
8
|
import { customElement, property } from "lit/decorators.js";
|
|
8
9
|
|
|
@@ -142,6 +143,12 @@ let EFText = class EFText$1 extends EFTemporal(LitElement) {
|
|
|
142
143
|
this.emitContentChange("content");
|
|
143
144
|
this.splitText();
|
|
144
145
|
}
|
|
146
|
+
if (changedProperties.has("_durationMs") && this.parentTimegroup) {
|
|
147
|
+
flushSequenceDurationCache();
|
|
148
|
+
flushStartTimeMsCache();
|
|
149
|
+
this.parentTimegroup.requestUpdate("durationMs");
|
|
150
|
+
this.parentTimegroup.requestUpdate("currentTime");
|
|
151
|
+
}
|
|
145
152
|
}
|
|
146
153
|
setupMutationObserver() {
|
|
147
154
|
this.mutationObserver = new MutationObserver(() => {
|
|
@@ -176,12 +183,18 @@ let EFText = class EFText$1 extends EFTemporal(LitElement) {
|
|
|
176
183
|
if (fingerprint === this.#lastPropagatedAnimation) return;
|
|
177
184
|
this.#lastPropagatedAnimation = fingerprint;
|
|
178
185
|
const hasAnimation = animationName && animationName !== "none";
|
|
179
|
-
for (const segment of segments)
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
186
|
+
for (const segment of segments) {
|
|
187
|
+
if (segment.hasAttribute("data-whitespace")) {
|
|
188
|
+
for (const prop of animationPropsToPropagate) segment.style.removeProperty(prop);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (hasAnimation) for (const prop of animationPropsToPropagate) {
|
|
192
|
+
let value = computed.getPropertyValue(prop);
|
|
193
|
+
if (prop === "animation-fill-mode" && value === "none") value = "backwards";
|
|
194
|
+
segment.style.setProperty(prop, value);
|
|
195
|
+
}
|
|
196
|
+
else for (const prop of animationPropsToPropagate) segment.style.removeProperty(prop);
|
|
183
197
|
}
|
|
184
|
-
else for (const prop of animationPropsToPropagate) segment.style.removeProperty(prop);
|
|
185
198
|
}
|
|
186
199
|
getTextContent() {
|
|
187
200
|
let text = "";
|
|
@@ -335,6 +348,7 @@ let EFText = class EFText$1 extends EFTemporal(LitElement) {
|
|
|
335
348
|
this.#lastPropagatedAnimation = "";
|
|
336
349
|
this.propagateAnimationToSegments();
|
|
337
350
|
if (useTemplate) for (const segment of this.segments) {
|
|
351
|
+
if (segment.hasAttribute("data-whitespace")) continue;
|
|
338
352
|
const computedFill = window.getComputedStyle(segment).getPropertyValue("animation-fill-mode");
|
|
339
353
|
if (computedFill === "none" || computedFill === "") segment.style.setProperty("animation-fill-mode", "backwards");
|
|
340
354
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EFText.js","names":["EFText","textNodes: ChildNode[]","#segmentsInitialized","segments","#lastPropagatedAnimation","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\";\n\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// Animation properties to propagate from ef-text to its segments.\n// animation-delay is intentionally excluded -- the stagger system manages delay per segment.\nconst animationPropsToPropagate = [\n \"animation-name\",\n \"animation-duration\",\n \"animation-timing-function\",\n \"animation-fill-mode\",\n \"animation-iteration-count\",\n \"animation-direction\",\n] as const;\n\n@customElement(\"ef-text\")\nexport class EFText extends EFTemporal(LitElement) {\n static styles = [\n css`\n :host {\n display: inline;\n }\n :host([split=\"line\"]) {\n display: inline-block;\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 animationObserver?: 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 #lastPropagatedAnimation = \"\";\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 if (this.isConnected) {\n this.emitContentChange(\"content\");\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 RAF to ensure DOM is fully ready before splitting text\n // Callers that need segments immediately (e.g., render clones) should await whenSegmentsReady()\n requestAnimationFrame(() => {\n this.setupMutationObserver();\n this.setupAnimationObserver();\n this.splitText();\n });\n }\n\n disconnectedCallback() {\n super.disconnectedCallback();\n this.mutationObserver?.disconnect();\n this.animationObserver?.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.emitContentChange(\"content\");\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 setupAnimationObserver() {\n this.animationObserver = new MutationObserver(() => {\n this.propagateAnimationToSegments();\n });\n\n this.animationObserver.observe(this, {\n attributes: true,\n attributeFilter: [\"class\", \"style\"],\n });\n }\n\n private propagateAnimationToSegments() {\n const segments = this.segments;\n if (segments.length === 0) return;\n\n const computed = window.getComputedStyle(this);\n const animationName = computed.animationName;\n\n // Build a fingerprint to avoid redundant work\n const fingerprint = animationPropsToPropagate\n .map((prop) => computed.getPropertyValue(prop))\n .join(\"|\");\n\n if (fingerprint === this.#lastPropagatedAnimation) return;\n this.#lastPropagatedAnimation = fingerprint;\n\n const hasAnimation = animationName && animationName !== \"none\";\n\n for (const segment of segments) {\n if (hasAnimation) {\n for (const prop of animationPropsToPropagate) {\n let value = computed.getPropertyValue(prop);\n if (prop === \"animation-fill-mode\" && value === \"none\") {\n value = \"backwards\";\n }\n segment.style.setProperty(prop, value);\n }\n } else {\n for (const prop of animationPropsToPropagate) {\n segment.style.removeProperty(prop);\n }\n }\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\n // GUARD: Check if segments are already correct before clearing/recreating\n // This prevents redundant splits from RAF callbacks, updated(), etc.\n if (\n this.#segmentsInitialized &&\n this.segments.length > 0 &&\n this.lastTextContent === text\n ) {\n return;\n }\n\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 if (/^\\s+$/.test(segmentText)) {\n segment.setAttribute(\"data-whitespace\", \"\");\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.style.whiteSpace = \"nowrap\";\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 if (/^\\s+$/.test(segmentText)) {\n segment.setAttribute(\"data-whitespace\", \"\");\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.style.whiteSpace = \"nowrap\";\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 // Propagate animation properties from this element to newly created segments\n this.#lastPropagatedAnimation = \"\";\n this.propagateAnimationToSegments();\n\n // For template-path segments: their animation comes from a class, not from ef-text.\n // propagateAnimationToSegments() is a no-op when ef-text has no animation, but its\n // else-branch removes all animation inline styles — so we apply the fill-mode default\n // after propagation to avoid being cleared.\n if (useTemplate) {\n for (const segment of this.segments) {\n const computedFill = window\n .getComputedStyle(segment)\n .getPropertyValue(\"animation-fill-mode\");\n if (computedFill === \"none\" || computedFill === \"\") {\n segment.style.setProperty(\"animation-fill-mode\", \"backwards\");\n }\n }\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 // Mark segments as initialized to prevent redundant splits\n this.#segmentsInitialized = true;\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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"}
|
|
1
|
+
{"version":3,"file":"EFText.js","names":["EFText","textNodes: ChildNode[]","#segmentsInitialized","segments","#lastPropagatedAnimation","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, flushStartTimeMsCache } from \"./EFTemporal.js\";\nimport { flushSequenceDurationCache } from \"./EFTimegroup.js\";\n\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// Animation properties to propagate from ef-text to its segments.\n// animation-delay is intentionally excluded -- the stagger system manages delay per segment.\nconst animationPropsToPropagate = [\n \"animation-name\",\n \"animation-duration\",\n \"animation-timing-function\",\n \"animation-fill-mode\",\n \"animation-iteration-count\",\n \"animation-direction\",\n] as const;\n\n@customElement(\"ef-text\")\nexport class EFText extends EFTemporal(LitElement) {\n static styles = [\n css`\n :host {\n display: inline;\n }\n :host([split=\"line\"]) {\n display: inline-block;\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 animationObserver?: 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 #lastPropagatedAnimation = \"\";\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 if (this.isConnected) {\n this.emitContentChange(\"content\");\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 RAF to ensure DOM is fully ready before splitting text\n // Callers that need segments immediately (e.g., render clones) should await whenSegmentsReady()\n requestAnimationFrame(() => {\n this.setupMutationObserver();\n this.setupAnimationObserver();\n this.splitText();\n });\n }\n\n disconnectedCallback() {\n super.disconnectedCallback();\n this.mutationObserver?.disconnect();\n this.animationObserver?.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.emitContentChange(\"content\");\n this.splitText();\n }\n\n if (changedProperties.has(\"_durationMs\") && this.parentTimegroup) {\n flushSequenceDurationCache();\n flushStartTimeMsCache();\n this.parentTimegroup.requestUpdate(\"durationMs\");\n this.parentTimegroup.requestUpdate(\"currentTime\");\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 setupAnimationObserver() {\n this.animationObserver = new MutationObserver(() => {\n this.propagateAnimationToSegments();\n });\n\n this.animationObserver.observe(this, {\n attributes: true,\n attributeFilter: [\"class\", \"style\"],\n });\n }\n\n private propagateAnimationToSegments() {\n const segments = this.segments;\n if (segments.length === 0) return;\n\n const computed = window.getComputedStyle(this);\n const animationName = computed.animationName;\n\n // Build a fingerprint to avoid redundant work\n const fingerprint = animationPropsToPropagate\n .map((prop) => computed.getPropertyValue(prop))\n .join(\"|\");\n\n if (fingerprint === this.#lastPropagatedAnimation) return;\n this.#lastPropagatedAnimation = fingerprint;\n\n const hasAnimation = animationName && animationName !== \"none\";\n\n for (const segment of segments) {\n // Whitespace segments are layout spacers — they must not receive animation.\n // Animating them (especially with opacity/transform keyframes and backwards\n // fill-mode) causes spaces to visually collapse between animated words/chars.\n if (segment.hasAttribute(\"data-whitespace\")) {\n for (const prop of animationPropsToPropagate) {\n segment.style.removeProperty(prop);\n }\n continue;\n }\n\n if (hasAnimation) {\n for (const prop of animationPropsToPropagate) {\n let value = computed.getPropertyValue(prop);\n if (prop === \"animation-fill-mode\" && value === \"none\") {\n value = \"backwards\";\n }\n segment.style.setProperty(prop, value);\n }\n } else {\n for (const prop of animationPropsToPropagate) {\n segment.style.removeProperty(prop);\n }\n }\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\n // GUARD: Check if segments are already correct before clearing/recreating\n // This prevents redundant splits from RAF callbacks, updated(), etc.\n if (\n this.#segmentsInitialized &&\n this.segments.length > 0 &&\n this.lastTextContent === text\n ) {\n return;\n }\n\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 if (/^\\s+$/.test(segmentText)) {\n segment.setAttribute(\"data-whitespace\", \"\");\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.style.whiteSpace = \"nowrap\";\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 if (/^\\s+$/.test(segmentText)) {\n segment.setAttribute(\"data-whitespace\", \"\");\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.style.whiteSpace = \"nowrap\";\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 // Propagate animation properties from this element to newly created segments\n this.#lastPropagatedAnimation = \"\";\n this.propagateAnimationToSegments();\n\n // For template-path segments: their animation comes from a class, not from ef-text.\n // propagateAnimationToSegments() is a no-op when ef-text has no animation, but its\n // else-branch removes all animation inline styles — so we apply the fill-mode default\n // after propagation to avoid being cleared.\n if (useTemplate) {\n for (const segment of this.segments) {\n if (segment.hasAttribute(\"data-whitespace\")) continue;\n const computedFill = window\n .getComputedStyle(segment)\n .getPropertyValue(\"animation-fill-mode\");\n if (computedFill === \"none\" || computedFill === \"\") {\n segment.style.setProperty(\"animation-fill-mode\", \"backwards\");\n }\n }\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 // Mark segments as initialized to prevent redundant splits\n this.#segmentsInitialized = true;\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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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EFTextSegment.js","names":["EFTextSegment"],"sources":["../../src/elements/EFTextSegment.ts"],"sourcesContent":["import { css, html, LitElement } from \"lit\";\nimport { customElement, property } from \"lit/decorators.js\";\nimport { EFTemporal } from \"./EFTemporal.ts\";\nimport { EFText } from \"./EFText.js\";\n\n@customElement(\"ef-text-segment\")\nexport class EFTextSegment extends EFTemporal(LitElement) {\n static styles = [\n css`\n :host {\n display: inline-block;\n }\n :host([data-whitespace]) {\n display: inline;\n }\n :host([data-line-segment]) {\n display: block;\n }\n :host([hidden]) {\n opacity: 0;\n pointer-events: none;\n }\n `,\n ];\n\n render() {\n // Set CSS variables in render() to ensure they're always set\n // This is necessary because Lit may clear inline styles during updates\n this.setCSSVariables();\n return html`${this.segmentText}`;\n }\n\n private setCSSVariables(): void {\n // Set deterministic --ef-seed value based on segment index\n const seed = (this.segmentIndex * 9007) % 233; // Prime numbers for better distribution\n const seedValue = seed / 233; // Normalize to 0-1 range\n this.style.setProperty(\"--ef-seed\", seedValue.toString());\n\n // Set stagger offset CSS variable\n // staggerOffsetMs is always set (defaults to 0), so we can always set the CSS variable\n const offsetMs = this.staggerOffsetMs ?? 0;\n this.style.setProperty(\"--ef-stagger-offset\", `${offsetMs}ms`);\n\n // Set index CSS variable\n this.style.setProperty(\"--ef-index\", this.segmentIndex.toString());\n }\n\n protected firstUpdated(): void {\n this.setCSSVariables();\n }\n\n protected updated(): void {\n this.setCSSVariables();\n }\n\n @property({ type: String, attribute: false })\n segmentText = \"\";\n\n @property({ type: Number, attribute: false })\n segmentIndex = 0;\n\n @property({ type: Number, attribute: false })\n staggerOffsetMs?: number;\n\n @property({ type: Number, attribute: false })\n segmentStartMs = 0;\n\n @property({ type: Number, attribute: false })\n segmentEndMs = 0;\n\n @property({ type: Boolean, reflect: true })\n hidden = false;\n\n get startTimeMs() {\n const parentText = this.closest(\"ef-text\") as EFText;\n const parentStartTime = parentText?.startTimeMs || 0;\n return parentStartTime + (this.segmentStartMs || 0);\n }\n\n get endTimeMs() {\n // Derive from parent's live durationMs rather than the snapshot stored in segmentEndMs.\n // This ensures segments track changes when the parent's duration updates\n // (e.g., a contain-mode timegroup whose duration changes after a video loads).\n const parentText = this.closest(\"ef-text\") as EFText;\n if (parentText) {\n return parentText.startTimeMs + parentText.durationMs;\n }\n return this.segmentEndMs || 0;\n }\n\n get durationMs(): number {\n const parentText = this.closest(\"ef-text\") as EFText;\n if (parentText) {\n return parentText.durationMs;\n }\n return this.segmentEndMs - this.segmentStartMs;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-text-segment\": EFTextSegment;\n }\n}\n"],"mappings":";;;;;;AAMO,0BAAMA,wBAAsB,WAAW,WAAW,CAAC;;;
|
|
1
|
+
{"version":3,"file":"EFTextSegment.js","names":["EFTextSegment"],"sources":["../../src/elements/EFTextSegment.ts"],"sourcesContent":["import { css, html, LitElement } from \"lit\";\nimport { customElement, property } from \"lit/decorators.js\";\nimport { EFTemporal } from \"./EFTemporal.ts\";\nimport { EFText } from \"./EFText.js\";\n\n@customElement(\"ef-text-segment\")\nexport class EFTextSegment extends EFTemporal(LitElement) {\n static styles = [\n css`\n :host {\n display: inline-block;\n }\n :host([data-whitespace]) {\n display: inline;\n white-space: pre;\n }\n :host([data-line-segment]) {\n display: block;\n }\n :host([hidden]) {\n opacity: 0;\n pointer-events: none;\n }\n `,\n ];\n\n render() {\n // Set CSS variables in render() to ensure they're always set\n // This is necessary because Lit may clear inline styles during updates\n this.setCSSVariables();\n return html`${this.segmentText}`;\n }\n\n private setCSSVariables(): void {\n // Set deterministic --ef-seed value based on segment index\n const seed = (this.segmentIndex * 9007) % 233; // Prime numbers for better distribution\n const seedValue = seed / 233; // Normalize to 0-1 range\n this.style.setProperty(\"--ef-seed\", seedValue.toString());\n\n // Set stagger offset CSS variable\n // staggerOffsetMs is always set (defaults to 0), so we can always set the CSS variable\n const offsetMs = this.staggerOffsetMs ?? 0;\n this.style.setProperty(\"--ef-stagger-offset\", `${offsetMs}ms`);\n\n // Set index CSS variable\n this.style.setProperty(\"--ef-index\", this.segmentIndex.toString());\n }\n\n protected firstUpdated(): void {\n this.setCSSVariables();\n }\n\n protected updated(): void {\n this.setCSSVariables();\n }\n\n @property({ type: String, attribute: false })\n segmentText = \"\";\n\n @property({ type: Number, attribute: false })\n segmentIndex = 0;\n\n @property({ type: Number, attribute: false })\n staggerOffsetMs?: number;\n\n @property({ type: Number, attribute: false })\n segmentStartMs = 0;\n\n @property({ type: Number, attribute: false })\n segmentEndMs = 0;\n\n @property({ type: Boolean, reflect: true })\n hidden = false;\n\n get startTimeMs() {\n const parentText = this.closest(\"ef-text\") as EFText;\n const parentStartTime = parentText?.startTimeMs || 0;\n return parentStartTime + (this.segmentStartMs || 0);\n }\n\n get endTimeMs() {\n // Derive from parent's live durationMs rather than the snapshot stored in segmentEndMs.\n // This ensures segments track changes when the parent's duration updates\n // (e.g., a contain-mode timegroup whose duration changes after a video loads).\n const parentText = this.closest(\"ef-text\") as EFText;\n if (parentText) {\n return parentText.startTimeMs + parentText.durationMs;\n }\n return this.segmentEndMs || 0;\n }\n\n get durationMs(): number {\n const parentText = this.closest(\"ef-text\") as EFText;\n if (parentText) {\n return parentText.durationMs;\n }\n return this.segmentEndMs - this.segmentStartMs;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-text-segment\": EFTextSegment;\n }\n}\n"],"mappings":";;;;;;AAMO,0BAAMA,wBAAsB,WAAW,WAAW,CAAC;;;qBAmD1C;sBAGC;wBAME;sBAGF;gBAGN;;;gBAjEO,CACd,GAAG;;;;;;;;;;;;;;;MAgBJ;;CAED,SAAS;AAGP,OAAK,iBAAiB;AACtB,SAAO,IAAI,GAAG,KAAK;;CAGrB,AAAQ,kBAAwB;EAG9B,MAAM,YADQ,KAAK,eAAe,OAAQ,MACjB;AACzB,OAAK,MAAM,YAAY,aAAa,UAAU,UAAU,CAAC;EAIzD,MAAM,WAAW,KAAK,mBAAmB;AACzC,OAAK,MAAM,YAAY,uBAAuB,GAAG,SAAS,IAAI;AAG9D,OAAK,MAAM,YAAY,cAAc,KAAK,aAAa,UAAU,CAAC;;CAGpE,AAAU,eAAqB;AAC7B,OAAK,iBAAiB;;CAGxB,AAAU,UAAgB;AACxB,OAAK,iBAAiB;;CAqBxB,IAAI,cAAc;AAGhB,UAFmB,KAAK,QAAQ,UAAU,EACN,eAAe,MACzB,KAAK,kBAAkB;;CAGnD,IAAI,YAAY;EAId,MAAM,aAAa,KAAK,QAAQ,UAAU;AAC1C,MAAI,WACF,QAAO,WAAW,cAAc,WAAW;AAE7C,SAAO,KAAK,gBAAgB;;CAG9B,IAAI,aAAqB;EACvB,MAAM,aAAa,KAAK,QAAQ,UAAU;AAC1C,MAAI,WACF,QAAO,WAAW;AAEpB,SAAO,KAAK,eAAe,KAAK;;;YAxCjC,SAAS;CAAE,MAAM;CAAQ,WAAW;CAAO,CAAC;YAG5C,SAAS;CAAE,MAAM;CAAQ,WAAW;CAAO,CAAC;YAG5C,SAAS;CAAE,MAAM;CAAQ,WAAW;CAAO,CAAC;YAG5C,SAAS;CAAE,MAAM;CAAQ,WAAW;CAAO,CAAC;YAG5C,SAAS;CAAE,MAAM;CAAQ,WAAW;CAAO,CAAC;YAG5C,SAAS;CAAE,MAAM;CAAS,SAAS;CAAM,CAAC;4BAlE5C,cAAc,kBAAkB"}
|
|
@@ -448,7 +448,7 @@ const mapAndSetAnimationTime = (animation, element, timing, effectiveDelay) => {
|
|
|
448
448
|
if (animation.playState === "running") animation.pause();
|
|
449
449
|
const adjustedTime = elementTime - effectiveDelay;
|
|
450
450
|
if (adjustedTime < 0) {
|
|
451
|
-
if (timing.delay > 0) animation.currentTime = elementTime;
|
|
451
|
+
if (timing.delay > 0) animation.currentTime = elementTime - (effectiveDelay - timing.delay);
|
|
452
452
|
else animation.currentTime = 0;
|
|
453
453
|
return;
|
|
454
454
|
}
|
|
@@ -456,14 +456,16 @@ const mapAndSetAnimationTime = (animation, element, timing, effectiveDelay) => {
|
|
|
456
456
|
const currentIteration = Math.floor(adjustedTime / duration);
|
|
457
457
|
if (currentIteration >= iterations) {
|
|
458
458
|
const completedAnimationTime = getCompletedAnimationTime(timing, calculateMaxSafeAnimationTime(duration, iterations));
|
|
459
|
-
if (timing.delay > 0) animation.currentTime =
|
|
459
|
+
if (timing.delay > 0) animation.currentTime = timing.delay + completedAnimationTime;
|
|
460
460
|
else animation.currentTime = completedAnimationTime;
|
|
461
461
|
} else {
|
|
462
462
|
const animationTime = mapElementTimeToAnimationTime(elementTime, timing, effectiveDelay);
|
|
463
463
|
const { direction, delay } = timing;
|
|
464
|
-
if (delay > 0)
|
|
465
|
-
|
|
466
|
-
|
|
464
|
+
if (delay > 0) {
|
|
465
|
+
const staggerShift = effectiveDelay - timing.delay;
|
|
466
|
+
if ((direction === "alternate" || direction === "alternate-reverse") && effectiveDelay > 0 && currentIteration === 0) animation.currentTime = elementTime - staggerShift;
|
|
467
|
+
else animation.currentTime = timing.delay + animationTime;
|
|
468
|
+
} else animation.currentTime = animationTime;
|
|
467
469
|
}
|
|
468
470
|
};
|
|
469
471
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"updateAnimations.js","names":["warnings: string[]","timeSource: AnimatableElement","staggerElement: AnimatableElement","node: Element | null","childContexts: ElementUpdateContext[]","visibleChildContexts: ElementUpdateContext[]"],"sources":["../../src/elements/updateAnimations.ts"],"sourcesContent":["import {\n deepGetTemporalElements,\n isEFTemporal,\n type TemporalMixinInterface,\n} from \"./EFTemporal.ts\";\n\n// All animatable elements are temporal elements with HTMLElement interface\nexport type AnimatableElement = TemporalMixinInterface & HTMLElement;\n\n// ============================================================================\n// Constants\n// ============================================================================\n\nconst ANIMATION_PRECISION_OFFSET = 0.1; // Use 0.1ms to safely avoid completion threshold\nconst DEFAULT_ANIMATION_ITERATIONS = 1;\nconst PROGRESS_PROPERTY = \"--ef-progress\";\nconst DURATION_PROPERTY = \"--ef-duration\";\nconst TRANSITION_DURATION_PROPERTY = \"--ef-transition-duration\";\nconst TRANSITION_OUT_START_PROPERTY = \"--ef-transition-out-start\";\n\n// ============================================================================\n// Animation Tracking\n// ============================================================================\n\n/**\n * Tracks animations per element to prevent them from being lost when they complete.\n * Once an animation reaches 100% completion, it's removed from getAnimations(),\n * but we keep a reference to it so we can continue controlling it.\n */\nconst animationTracker = new WeakMap<Element, Set<Animation>>();\n\n/**\n * Tracks whether DOM structure has changed for an element, requiring animation rediscovery.\n * For render clones (static DOM), this stays false after initial discovery.\n * For prime timeline (interactive), this is set to true when mutations occur.\n */\nconst domStructureChanged = new WeakMap<Element, boolean>();\n\n/**\n * Tracks the last known animation count for an element to detect new animations.\n * Used as a lightweight check before calling expensive getAnimations().\n */\nconst lastAnimationCount = new WeakMap<Element, number>();\n\n/**\n * Tracks which animations have already been validated to avoid duplicate warnings.\n * Uses animation name + duration as the unique key.\n */\nconst validatedAnimations = new Set<string>();\n\n/**\n * Validates that an animation is still valid and controllable.\n * Animations become invalid when:\n * - They've been cancelled (idle state and not in getAnimations())\n * - Their effect is null (animation was removed)\n * - Their target is no longer in the DOM\n */\nconst isAnimationValid = (\n animation: Animation,\n currentAnimations: Animation[],\n): boolean => {\n // Check if animation has been cancelled\n if (\n animation.playState === \"idle\" &&\n !currentAnimations.includes(animation)\n ) {\n return false;\n }\n\n // Check if animation effect is still valid\n const effect = animation.effect;\n if (!effect) {\n return false;\n }\n\n // Check if target is still in DOM\n if (effect instanceof KeyframeEffect) {\n const target = effect.target;\n if (target && target instanceof Element) {\n if (!target.isConnected) {\n return false;\n }\n }\n }\n\n return true;\n};\n\n/**\n * Discovers and tracks animations on an element and its subtree.\n * This ensures we have references to animations even after they complete.\n *\n * Tracks animations per element where they exist, not just on the root element.\n * This allows us to find animations on any element in the subtree.\n *\n * OPTIMIZATION: For render clones (static DOM), discovery happens once at creation.\n * For prime timeline (interactive), discovery is responsive to DOM changes.\n *\n * Also cleans up invalid animations (cancelled, removed from DOM, etc.)\n *\n * @param providedAnimations - Optional pre-discovered animations to avoid redundant getAnimations() calls\n */\nconst discoverAndTrackAnimations = (\n element: AnimatableElement,\n providedAnimations?: Animation[],\n): { tracked: Set<Animation>; current: Animation[] } => {\n animationTracker.has(element);\n const structureChanged = domStructureChanged.get(element) ?? true;\n\n // REMOVED: Clone optimization that cached animation references.\n // The optimization assumed animations were \"static\" for clones, but this was incorrect.\n // After seeking to a new time, we need fresh animation state from the browser.\n // Caching caused animations to be stuck at their discovery state (often 0ms).\n\n // For prime timeline or first discovery: get current animations from the browser (includes subtree)\n // CRITICAL: This is expensive, so we return it to avoid calling it again\n // If animations were provided by caller (to avoid redundant calls), use those\n const currentAnimations =\n providedAnimations ?? element.getAnimations({ subtree: true });\n\n // Mark structure as stable after discovery\n // This prevents redundant getAnimations() calls when DOM hasn't changed\n domStructureChanged.set(element, false);\n\n // Track animation count for lightweight change detection\n lastAnimationCount.set(element, currentAnimations.length);\n\n // Track animations on each element where they exist\n for (const animation of currentAnimations) {\n const effect = animation.effect;\n const target =\n effect && effect instanceof KeyframeEffect ? effect.target : null;\n if (target && target instanceof Element) {\n let tracked = animationTracker.get(target);\n if (!tracked) {\n tracked = new Set<Animation>();\n animationTracker.set(target, tracked);\n }\n tracked.add(animation);\n }\n }\n\n // Also maintain a set on the root element for coordination\n let rootTracked = animationTracker.get(element);\n if (!rootTracked) {\n rootTracked = new Set<Animation>();\n animationTracker.set(element, rootTracked);\n }\n\n // Update root set with all current animations\n for (const animation of currentAnimations) {\n rootTracked.add(animation);\n }\n\n // Clean up invalid animations from root set\n // This handles animations that were cancelled, removed from DOM, or had their effects removed\n for (const animation of rootTracked) {\n if (!isAnimationValid(animation, currentAnimations)) {\n rootTracked.delete(animation);\n }\n }\n\n // Build a map of element -> current animations from the subtree lookup we already did\n // This avoids calling getAnimations() repeatedly on each element (expensive!)\n const elementAnimationsMap = new Map<Element, Animation[]>();\n for (const animation of currentAnimations) {\n const effect = animation.effect;\n const target =\n effect && effect instanceof KeyframeEffect ? effect.target : null;\n if (target && target instanceof Element) {\n let anims = elementAnimationsMap.get(target);\n if (!anims) {\n anims = [];\n elementAnimationsMap.set(target, anims);\n }\n anims.push(animation);\n }\n }\n\n // Clean up invalid animations from per-element sets.\n // Only walk the full subtree when DOM structure has changed (elements added/removed).\n // During scrubbing with static DOM, skip this expensive querySelectorAll(\"*\").\n if (structureChanged) {\n for (const [el, tracked] of elementAnimationsMap) {\n const existingTracked = animationTracker.get(el);\n if (existingTracked) {\n for (const animation of existingTracked) {\n if (!isAnimationValid(animation, tracked)) {\n existingTracked.delete(animation);\n }\n }\n if (existingTracked.size === 0) {\n animationTracker.delete(el);\n }\n }\n }\n }\n\n return { tracked: rootTracked, current: currentAnimations };\n};\n\n/**\n * Cleans up tracked animations when an element is disconnected.\n * This prevents memory leaks.\n */\nexport const cleanupTrackedAnimations = (element: Element): void => {\n animationTracker.delete(element);\n domStructureChanged.delete(element);\n lastAnimationCount.delete(element);\n};\n\n/**\n * Marks that DOM structure has changed for an element, requiring animation rediscovery.\n * Should be called when elements are added/removed or CSS classes change that affect animations.\n */\nexport const markDomStructureChanged = (element: Element): void => {\n domStructureChanged.set(element, true);\n};\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Represents the phase an element is in relative to the timeline.\n * This is the primary concept that drives all visibility and animation decisions.\n */\nexport type ElementPhase =\n | \"before-start\"\n | \"active\"\n | \"at-end-boundary\"\n | \"after-end\";\n\n/**\n * Represents the temporal state of an element relative to the timeline\n */\ninterface TemporalState {\n progress: number;\n isVisible: boolean;\n timelineTimeMs: number;\n phase: ElementPhase;\n}\n\n/**\n * Context object that holds all evaluated state for an element update.\n * This groups related state together, reducing parameter passing and making\n * the data flow clearer.\n */\ninterface ElementUpdateContext {\n element: AnimatableElement;\n state: TemporalState;\n}\n\n/**\n * Animation timing information extracted from an animation effect.\n * Groups related timing properties together.\n */\ninterface AnimationTiming {\n duration: number;\n delay: number;\n iterations: number;\n direction: string;\n}\n\n/**\n * Capability interface for elements that support stagger offset.\n * This encapsulates the stagger behavior behind a capability check rather than\n * leaking tag name checks throughout the codebase.\n */\ninterface StaggerableElement extends AnimatableElement {\n staggerOffsetMs?: number;\n}\n\n// ============================================================================\n// Phase Determination\n// ============================================================================\n\n/**\n * Determines what phase an element is in relative to the timeline.\n *\n * WHY: Phase is the primary concept that drives all decisions. By explicitly\n * enumerating phases, we make the code's logic clear: phase determines visibility,\n * animation coordination, and visual state.\n *\n * Phases:\n * - before-start: Timeline is before element's start time\n * - active: Timeline is within element's active range (start to end, exclusive of end)\n * - at-end-boundary: Timeline is exactly at element's end time\n * - after-end: Timeline is after element's end time\n *\n * Note: We detect \"at-end-boundary\" by checking if timeline equals end time.\n * The boundary policy will then determine if this should be treated as visible/active\n * or not based on element characteristics.\n */\nconst determineElementPhase = (\n element: AnimatableElement,\n timelineTimeMs: number,\n): ElementPhase => {\n // Read endTimeMs once to avoid recalculation issues\n const endTimeMs = element.endTimeMs;\n const startTimeMs = element.startTimeMs;\n\n // Invalid range (end <= start) means element hasn't computed its duration yet,\n // or has no temporal children (e.g., timegroup with only static HTML).\n // Treat as always active - these elements should be visible at all times.\n if (endTimeMs <= startTimeMs) {\n return \"active\";\n }\n\n if (timelineTimeMs < startTimeMs) {\n return \"before-start\";\n }\n // Use epsilon to handle floating point precision issues\n const epsilon = 0.001;\n const diff = timelineTimeMs - endTimeMs;\n\n // If clearly after end (difference > epsilon), return 'after-end'\n if (diff > epsilon) {\n return \"after-end\";\n }\n // If at or very close to end boundary (within epsilon), return 'at-end-boundary'\n if (Math.abs(diff) <= epsilon) {\n return \"at-end-boundary\";\n }\n // Otherwise, we're before the end, so check if we're active\n return \"active\";\n};\n\n// ============================================================================\n// Boundary Policies\n// ============================================================================\n\n/**\n * Policy interface for determining behavior at boundaries.\n * Different policies apply different rules for when elements should be visible\n * or have animations coordinated at exact boundary times.\n */\ninterface BoundaryPolicy {\n /**\n * Determines if an element should be considered visible/active at the end boundary\n * based on the element's characteristics.\n */\n shouldIncludeEndBoundary(element: AnimatableElement): boolean;\n}\n\n/**\n * Visibility policy: determines when elements should be visible for display purposes.\n *\n * WHY: Root elements, elements aligned with composition end, and text segments\n * should remain visible at exact end time to prevent flicker and show final frames.\n * Other elements use exclusive end for clean transitions between elements.\n */\nclass VisibilityPolicy implements BoundaryPolicy {\n shouldIncludeEndBoundary(element: AnimatableElement): boolean {\n // Root elements should remain visible at exact end time to prevent flicker\n const isRootElement = !element.parentTimegroup;\n if (isRootElement) {\n return true;\n }\n\n // Elements aligned with composition end should remain visible at exact end time\n const isLastElementInComposition =\n element.endTimeMs === element.rootTimegroup?.endTimeMs;\n if (isLastElementInComposition) {\n return true;\n }\n\n // Text segments use inclusive end since they're meant to be visible for full duration\n if (this.isTextSegment(element)) {\n return true;\n }\n\n // Other elements use exclusive end for clean transitions\n return false;\n }\n\n /**\n * Checks if element is a text segment.\n * Encapsulates the tag name check to hide implementation detail.\n */\n protected isTextSegment(element: AnimatableElement): boolean {\n return element.tagName === \"EF-TEXT-SEGMENT\";\n }\n}\n\n// Policy instances (singleton pattern for stateless policies)\nconst visibilityPolicy = new VisibilityPolicy();\n\n/**\n * Determines if an element should be visible based on its phase and visibility policy.\n */\nconst shouldBeVisible = (\n phase: ElementPhase,\n element: AnimatableElement,\n): boolean => {\n if (phase === \"before-start\" || phase === \"after-end\") {\n return false;\n }\n if (phase === \"active\") {\n return true;\n }\n // phase === \"at-end-boundary\"\n return visibilityPolicy.shouldIncludeEndBoundary(element);\n};\n\n/**\n * Determines if animations should be coordinated based on element phase and animation policy.\n *\n * CRITICAL: Always returns true to support scrubbing to arbitrary times.\n *\n * Previously, this function skipped coordination for before-start and after-end phases as an\n * optimization for live playback. However, this broke scrubbing scenarios where we seek to\n * arbitrary times (timeline scrubbing, thumbnails, video export).\n *\n * The performance cost of always coordinating is minimal:\n * - Animations only update when element time changes\n * - Paused animation updates are optimized by the browser\n * - The benefit is correct animation state at all times, regardless of phase\n */\nconst shouldCoordinateAnimations = (\n _phase: ElementPhase,\n _element: AnimatableElement,\n): boolean => {\n return true;\n};\n\n// ============================================================================\n// Temporal State Evaluation\n// ============================================================================\n\n/**\n * Evaluates what the element's state should be based on the timeline.\n *\n * WHY: This function determines the complete temporal state including phase,\n * which becomes the primary driver for all subsequent decisions.\n */\nexport const evaluateTemporalState = (\n element: AnimatableElement,\n): TemporalState => {\n // Get timeline time from root timegroup, or use element's own time if it IS a timegroup\n const timelineTimeMs = (element.rootTimegroup ?? element).currentTimeMs;\n\n const progress =\n element.durationMs <= 0\n ? 1\n : Math.max(0, Math.min(1, element.currentTimeMs / element.durationMs));\n\n const phase = determineElementPhase(element, timelineTimeMs);\n const isVisible = shouldBeVisible(phase, element);\n\n return { progress, isVisible, timelineTimeMs, phase };\n};\n\n/**\n * Evaluates element visibility state specifically for animation coordination.\n * Uses inclusive end boundaries to prevent animation jumps at exact boundaries.\n *\n * This is exported for external use cases that need animation-specific visibility\n * evaluation without the full ElementUpdateContext.\n */\nexport const evaluateAnimationVisibilityState = (\n element: AnimatableElement,\n): TemporalState => {\n const state = evaluateTemporalState(element);\n // Override visibility based on animation policy\n const shouldCoordinate = shouldCoordinateAnimations(state.phase, element);\n return { ...state, isVisible: shouldCoordinate };\n};\n\n// ============================================================================\n// Animation Time Mapping\n// ============================================================================\n\n/**\n * Capability check: determines if an element supports stagger offset.\n * Encapsulates the knowledge of which element types support this feature.\n */\nconst supportsStaggerOffset = (\n element: AnimatableElement,\n): element is StaggerableElement => {\n // Currently only text segments support stagger offset\n return element.tagName === \"EF-TEXT-SEGMENT\";\n};\n\n/**\n * Calculates effective delay including stagger offset if applicable.\n *\n * Stagger offset allows elements (like text segments) to have their animations\n * start at different times while keeping their visibility timing unchanged.\n * This enables staggered animation effects within a single timegroup.\n */\nconst calculateEffectiveDelay = (\n delay: number,\n element: AnimatableElement,\n): number => {\n if (supportsStaggerOffset(element)) {\n // Read stagger offset - try property first (more reliable), then CSS variable\n // The staggerOffsetMs property is set directly on the element and is always available\n const segment = element as any;\n if (\n segment.staggerOffsetMs !== undefined &&\n segment.staggerOffsetMs !== null\n ) {\n return delay + segment.staggerOffsetMs;\n }\n\n // Fallback to CSS variable if property not available\n let cssValue = (element as HTMLElement).style\n .getPropertyValue(\"--ef-stagger-offset\")\n .trim();\n\n if (!cssValue) {\n cssValue = window\n .getComputedStyle(element)\n .getPropertyValue(\"--ef-stagger-offset\")\n .trim();\n }\n\n if (cssValue) {\n // Parse \"100ms\" format to milliseconds\n const match = cssValue.match(/(\\d+(?:\\.\\d+)?)\\s*ms?/);\n if (match) {\n const staggerOffset = parseFloat(match[1]!);\n if (!isNaN(staggerOffset)) {\n return delay + staggerOffset;\n }\n } else {\n // Try parsing as just a number\n const numValue = parseFloat(cssValue);\n if (!isNaN(numValue)) {\n return delay + numValue;\n }\n }\n }\n }\n return delay;\n};\n\n/**\n * Calculates maximum safe animation time to prevent completion.\n *\n * WHY: Once an animation reaches \"finished\" state, it can no longer be manually controlled\n * via currentTime. By clamping to just before completion (using ANIMATION_PRECISION_OFFSET),\n * we ensure the animation remains in a controllable state, allowing us to synchronize it\n * with the timeline even when it would naturally be complete.\n */\nconst calculateMaxSafeAnimationTime = (\n duration: number,\n iterations: number,\n): number => {\n return duration * iterations - ANIMATION_PRECISION_OFFSET;\n};\n\n/**\n * Determines if the current iteration should be reversed based on direction\n */\nconst shouldReverseIteration = (\n direction: string,\n currentIteration: number,\n): boolean => {\n return (\n direction === \"reverse\" ||\n (direction === \"alternate\" && currentIteration % 2 === 1) ||\n (direction === \"alternate-reverse\" && currentIteration % 2 === 0)\n );\n};\n\n/**\n * Applies direction to iteration time (reverses if needed)\n */\nconst applyDirectionToIterationTime = (\n currentIterationTime: number,\n duration: number,\n direction: string,\n currentIteration: number,\n): number => {\n if (shouldReverseIteration(direction, currentIteration)) {\n return duration - currentIterationTime;\n }\n return currentIterationTime;\n};\n\n/**\n * Maps element time to animation time for normal direction.\n * Uses cumulative time throughout the animation.\n * Note: elementTime should already be adjusted (elementTime - effectiveDelay).\n */\nconst mapNormalDirectionTime = (\n elementTime: number,\n duration: number,\n maxSafeTime: number,\n): number => {\n const currentIteration = Math.floor(elementTime / duration);\n const iterationTime = elementTime % duration;\n const cumulativeTime = currentIteration * duration + iterationTime;\n return Math.min(cumulativeTime, maxSafeTime);\n};\n\n/**\n * Maps element time to animation time for reverse direction.\n * Uses cumulative time with reversed iterations.\n * Note: elementTime should already be adjusted (elementTime - effectiveDelay).\n */\nconst mapReverseDirectionTime = (\n elementTime: number,\n duration: number,\n maxSafeTime: number,\n): number => {\n const currentIteration = Math.floor(elementTime / duration);\n const rawIterationTime = elementTime % duration;\n const reversedIterationTime = duration - rawIterationTime;\n const cumulativeTime = currentIteration * duration + reversedIterationTime;\n return Math.min(cumulativeTime, maxSafeTime);\n};\n\n/**\n * Maps element time to animation time for alternate/alternate-reverse directions.\n *\n * WHY SPECIAL HANDLING: Alternate directions oscillate between forward and reverse iterations.\n * Without delay, we use iteration time (0 to duration) because the animation naturally\n * resets each iteration. However, with delay, iteration 0 needs to account for the delay\n * offset (using ownCurrentTimeMs), and later iterations need cumulative time to properly\n * track progress across multiple iterations. This complexity requires a dedicated mapper\n * rather than trying to handle it in the general case.\n */\nconst mapAlternateDirectionTime = (\n elementTime: number,\n effectiveDelay: number,\n duration: number,\n direction: string,\n maxSafeTime: number,\n): number => {\n const adjustedTime = elementTime - effectiveDelay;\n\n if (effectiveDelay > 0) {\n // With delay: iteration 0 uses elementTime to include delay offset,\n // later iterations use cumulative time to track progress across iterations\n const currentIteration = Math.floor(adjustedTime / duration);\n if (currentIteration === 0) {\n return Math.min(elementTime, maxSafeTime);\n }\n return Math.min(adjustedTime, maxSafeTime);\n }\n\n // Without delay: use iteration time (after direction applied) since animation\n // naturally resets each iteration\n const currentIteration = Math.floor(elementTime / duration);\n const rawIterationTime = elementTime % duration;\n const iterationTime = applyDirectionToIterationTime(\n rawIterationTime,\n duration,\n direction,\n currentIteration,\n );\n return Math.min(iterationTime, maxSafeTime);\n};\n\n/**\n * Maps element time to animation time based on direction.\n *\n * WHY: This function explicitly transforms element time to animation time, making\n * the time mapping concept clear. Different directions require different transformations\n * to achieve the desired visual effect.\n */\nconst mapElementTimeToAnimationTime = (\n elementTime: number,\n timing: AnimationTiming,\n effectiveDelay: number,\n): number => {\n const { duration, iterations, direction } = timing;\n const maxSafeTime = calculateMaxSafeAnimationTime(duration, iterations);\n // Calculate adjusted time (element time minus delay) for normal/reverse directions\n const adjustedTime = elementTime - effectiveDelay;\n\n if (direction === \"reverse\") {\n return mapReverseDirectionTime(adjustedTime, duration, maxSafeTime);\n }\n if (direction === \"alternate\" || direction === \"alternate-reverse\") {\n return mapAlternateDirectionTime(\n elementTime,\n effectiveDelay,\n duration,\n direction,\n maxSafeTime,\n );\n }\n // normal direction - use adjustedTime to account for delay\n return mapNormalDirectionTime(adjustedTime, duration, maxSafeTime);\n};\n\n/**\n * Determines the animation time for a completed animation based on direction.\n */\nconst getCompletedAnimationTime = (\n timing: AnimationTiming,\n maxSafeTime: number,\n): number => {\n const { direction, iterations, duration } = timing;\n\n if (direction === \"alternate\" || direction === \"alternate-reverse\") {\n // For alternate directions, determine if final iteration is reversed\n const finalIteration = iterations - 1;\n const isFinalIterationReversed =\n (direction === \"alternate\" && finalIteration % 2 === 1) ||\n (direction === \"alternate-reverse\" && finalIteration % 2 === 0);\n\n if (isFinalIterationReversed) {\n // At end of reversed iteration, currentTime should be near 0 (but clamped)\n return Math.min(duration - ANIMATION_PRECISION_OFFSET, maxSafeTime);\n }\n }\n\n // For normal, reverse, or forward final iteration of alternate: use max safe time\n return maxSafeTime;\n};\n\n/**\n * Validates that animation effect is a KeyframeEffect with a target\n */\nconst validateAnimationEffect = (\n effect: AnimationEffect | null,\n): effect is KeyframeEffect & { target: Element } => {\n return (\n effect !== null &&\n effect instanceof KeyframeEffect &&\n effect.target !== null\n );\n};\n\n/**\n * Extracts timing information from an animation effect.\n * Duration and delay from getTiming() are already in milliseconds.\n * We use getTiming().delay directly from the animation object.\n */\nconst extractAnimationTiming = (effect: KeyframeEffect): AnimationTiming => {\n const timing = effect.getTiming();\n\n return {\n duration: Number(timing.duration) || 0,\n delay: Number(timing.delay) || 0,\n iterations: Number(timing.iterations) || DEFAULT_ANIMATION_ITERATIONS,\n direction: timing.direction || \"normal\",\n };\n};\n\n// ============================================================================\n// Animation Fill Mode Validation (Development Mode)\n// ============================================================================\n\n/**\n * Analyzes keyframes to detect if animation is a fade-in or fade-out effect.\n * Returns 'fade-in', 'fade-out', 'both', or null.\n */\nconst detectFadePattern = (\n keyframes: Keyframe[],\n): \"fade-in\" | \"fade-out\" | \"both\" | null => {\n if (!keyframes || keyframes.length < 2) return null;\n\n const firstFrame = keyframes[0];\n const lastFrame = keyframes[keyframes.length - 1];\n\n const firstOpacity =\n firstFrame && \"opacity\" in firstFrame ? Number(firstFrame.opacity) : null;\n const lastOpacity =\n lastFrame && \"opacity\" in lastFrame ? Number(lastFrame.opacity) : null;\n\n if (firstOpacity === null || lastOpacity === null) return null;\n\n const isFadeIn = firstOpacity < lastOpacity;\n const isFadeOut = firstOpacity > lastOpacity;\n\n if (isFadeIn && isFadeOut) return \"both\";\n if (isFadeIn) return \"fade-in\";\n if (isFadeOut) return \"fade-out\";\n return null;\n};\n\n/**\n * Analyzes keyframes to detect if animation has transform changes (slide, scale, etc).\n */\nconst hasTransformAnimation = (keyframes: Keyframe[]): boolean => {\n if (!keyframes || keyframes.length < 2) return false;\n\n return keyframes.some(\n (frame) =>\n \"transform\" in frame ||\n \"translate\" in frame ||\n \"scale\" in frame ||\n \"rotate\" in frame,\n );\n};\n\n/**\n * Validates CSS animation fill-mode to prevent flashing issues.\n *\n * CRITICAL: Editframe's timeline system pauses animations and manually controls them\n * via animation.currentTime. This means elements exist in the DOM before their animations\n * start. Without proper fill-mode, elements will \"flash\" to their natural state before\n * the animation begins.\n *\n * Common issues:\n * - Delayed animations without 'backwards': Element shows natural state during delay\n * - Fade-in without 'backwards': Element visible before fade starts\n * - Fade-out without 'forwards': Element snaps back after fade completes\n *\n * Only runs in development mode to avoid performance impact in production.\n */\nconst validateAnimationFillMode = (\n animation: Animation,\n timing: AnimationTiming,\n): void => {\n // Only validate in development mode\n if (\n typeof process !== \"undefined\" &&\n process.env?.NODE_ENV === \"production\"\n ) {\n return;\n }\n\n const effect = animation.effect;\n if (!validateAnimationEffect(effect)) {\n return;\n }\n\n const effectTiming = effect.getTiming();\n const fill = effectTiming.fill || \"none\";\n const target = effect.target;\n\n // Get animation name for better error messages\n let animationName = \"unknown\";\n if (animation.id) {\n animationName = animation.id;\n } else if (target instanceof HTMLElement) {\n const computedStyle = window.getComputedStyle(target);\n const animationNameValue = computedStyle.animationName;\n if (animationNameValue && animationNameValue !== \"none\") {\n animationName = animationNameValue.split(\",\")[0]?.trim() || \"unknown\";\n }\n }\n\n // Create unique key based on animation name and duration\n const validationKey = `${animationName}-${timing.duration}`;\n\n // Skip if already validated\n if (validatedAnimations.has(validationKey)) {\n return;\n }\n validatedAnimations.add(validationKey);\n\n const warnings: string[] = [];\n\n // Check 1: Delayed animations without backwards/both\n if (timing.delay > 0 && fill !== \"backwards\" && fill !== \"both\") {\n warnings.push(\n `⚠️ Animation \"${animationName}\" has a ${timing.delay}ms delay but no 'backwards' fill-mode.`,\n ` This will cause the element to show its natural state during the delay, then suddenly jump when the animation starts.`,\n ` Fix: Add 'backwards' or 'both' to the animation shorthand.`,\n ` Example: animation: ${animationName} ${timing.duration}ms ${timing.delay}ms backwards;`,\n );\n }\n\n // Check 2: Analyze keyframes for fade/transform patterns\n try {\n const keyframes = effect.getKeyframes();\n const fadePattern = detectFadePattern(keyframes);\n const hasTransform = hasTransformAnimation(keyframes);\n\n // Fade-in or transform-in animations should use backwards\n if (\n (fadePattern === \"fade-in\" || hasTransform) &&\n fill !== \"backwards\" &&\n fill !== \"both\"\n ) {\n warnings.push(\n `⚠️ Animation \"${animationName}\" modifies initial state but lacks 'backwards' fill-mode.`,\n ` The element will be visible in its natural state before the animation starts.`,\n ` Fix: Add 'backwards' or 'both' to the animation.`,\n ` Example: animation: ${animationName} ${timing.duration}ms backwards;`,\n );\n }\n\n // Fade-out animations should use forwards\n if (fadePattern === \"fade-out\" && fill !== \"forwards\" && fill !== \"both\") {\n warnings.push(\n `⚠️ Animation \"${animationName}\" modifies final state but lacks 'forwards' fill-mode.`,\n ` The element will snap back to its natural state after the animation completes.`,\n ` Fix: Add 'forwards' or 'both' to the animation.`,\n ` Example: animation: ${animationName} ${timing.duration}ms forwards;`,\n );\n }\n\n // Combined effects should use both\n if (fadePattern === \"both\" && fill !== \"both\") {\n warnings.push(\n `⚠️ Animation \"${animationName}\" modifies both initial and final state but doesn't use 'both' fill-mode.`,\n ` Fix: Use 'both' to apply initial and final states.`,\n ` Example: animation: ${animationName} ${timing.duration}ms both;`,\n );\n }\n } catch (e) {\n // Silently skip keyframe analysis if it fails\n }\n\n if (warnings.length > 0 && typeof window !== \"undefined\") {\n console.groupCollapsed(\n \"%c🎬 Editframe Animation Fill-Mode Warning\",\n \"color: #f59e0b; font-weight: bold\",\n );\n warnings.forEach((warning) => console.log(warning));\n console.log(\n \"\\n📚 Learn more: https://developer.mozilla.org/en-US/docs/Web/CSS/animation-fill-mode\",\n );\n console.groupEnd();\n }\n};\n\n/**\n * Prepares animation for manual control by ensuring it's paused\n */\nconst prepareAnimation = (animation: Animation): void => {\n // Ensure animation is in a controllable state\n // Finished animations can't be controlled, so reset them\n if (animation.playState === \"finished\") {\n animation.cancel();\n // After cancel, animation is in idle state - we can set currentTime directly\n // No need to play/pause - we'll control it via currentTime\n } else if (animation.playState === \"running\") {\n // Pause running animations so we can control them manually\n animation.pause();\n }\n // For \"idle\" or \"paused\" state, we can set currentTime directly without play/pause\n // Setting currentTime on a paused animation will apply the keyframes\n // No initialization needed - we control everything via currentTime\n};\n\n/**\n * Maps element time to animation currentTime and sets it on the animation.\n *\n * WHY: This function explicitly performs the time mapping transformation,\n * making it clear that we're transforming element time to animation time.\n */\nconst mapAndSetAnimationTime = (\n animation: Animation,\n element: AnimatableElement,\n timing: AnimationTiming,\n effectiveDelay: number,\n): void => {\n // Use ownCurrentTimeMs for all elements (timegroups and other temporal elements)\n // This gives us time relative to when the element started, which ensures animations\n // on child elements are synchronized with their containing timegroup's timeline.\n // For timegroups, ownCurrentTimeMs is the time relative to when the timegroup started.\n // For other temporal elements, ownCurrentTimeMs is the time relative to their start.\n const elementTime = element.ownCurrentTimeMs ?? 0;\n\n // Ensure animation is paused before setting currentTime\n if (animation.playState === \"running\") {\n animation.pause();\n }\n\n // Calculate adjusted time (element time minus delay)\n const adjustedTime = elementTime - effectiveDelay;\n\n // If before delay, show initial keyframe state (0% of animation)\n if (adjustedTime < 0) {\n // Before delay: show initial keyframe state\n // For CSS animations with delay > 0, currentTime includes the delay, so set to elementTime\n // For CSS animations with delay = 0, currentTime is just animation progress, so set to 0\n if (timing.delay > 0) {\n animation.currentTime = elementTime;\n } else {\n animation.currentTime = 0;\n }\n return;\n }\n\n // At delay time (adjustedTime = 0) or after, the animation should be active\n const { duration, iterations } = timing;\n const currentIteration = Math.floor(adjustedTime / duration);\n\n if (currentIteration >= iterations) {\n // Animation is completed - use completed time mapping\n const maxSafeTime = calculateMaxSafeAnimationTime(duration, iterations);\n const completedAnimationTime = getCompletedAnimationTime(\n timing,\n maxSafeTime,\n );\n\n // CRITICAL: For CSS animations, currentTime behavior differs based on whether delay > 0:\n // - If timing.delay > 0: currentTime includes the delay (absolute timeline time)\n // - If timing.delay === 0: currentTime is just animation progress (0 to duration)\n if (timing.delay > 0) {\n // Completed: currentTime should be delay + completed animation time (absolute timeline time)\n animation.currentTime = effectiveDelay + completedAnimationTime;\n } else {\n // Completed: currentTime should be just the completed animation time (animation progress)\n animation.currentTime = completedAnimationTime;\n }\n } else {\n // Animation is in progress - map element time to animation time\n const animationTime = mapElementTimeToAnimationTime(\n elementTime,\n timing,\n effectiveDelay,\n );\n\n // CRITICAL: For CSS animations, currentTime behavior differs based on whether delay > 0:\n // - If timing.delay > 0: currentTime includes the delay (absolute timeline time)\n // - If timing.delay === 0: currentTime is just animation progress (0 to duration)\n // Stagger offset is handled via adjustedTime calculation, but doesn't affect currentTime format\n const { direction, delay } = timing;\n\n if (delay > 0) {\n // CSS animation with delay: currentTime is absolute timeline time\n const isAlternateWithDelay =\n (direction === \"alternate\" || direction === \"alternate-reverse\") &&\n effectiveDelay > 0;\n if (isAlternateWithDelay && currentIteration === 0) {\n // For alternate direction iteration 0 with delay, use elementTime directly\n animation.currentTime = elementTime;\n } else {\n // For other cases with delay, currentTime should be delay + animation time (absolute timeline time)\n animation.currentTime = effectiveDelay + animationTime;\n }\n } else {\n // CSS animation with delay = 0: currentTime is just animation progress\n // Stagger offset is already accounted for in adjustedTime, so animationTime is the progress\n animation.currentTime = animationTime;\n }\n }\n};\n\n/**\n * Synchronizes a single animation with the timeline using the element as the time source.\n *\n * For animations in this element's subtree, always use this element as the time source.\n * This handles both animations directly on the temporal element and on its non-temporal children.\n */\nconst synchronizeAnimation = (\n animation: Animation,\n element: AnimatableElement,\n): void => {\n const effect = animation.effect;\n if (!validateAnimationEffect(effect)) {\n return;\n }\n\n const timing = extractAnimationTiming(effect);\n\n if (timing.duration <= 0) {\n animation.currentTime = 0;\n return;\n }\n\n // Validate fill-mode in development mode\n validateAnimationFillMode(animation, timing);\n\n // Find the containing timegroup for the animation target.\n // Temporal elements are always synced to timegroups, so animations should use\n // the timegroup's timeline as the time source.\n const target = effect.target;\n let timeSource: AnimatableElement = element;\n\n if (target && target instanceof HTMLElement) {\n // Find the nearest timegroup in the DOM tree\n const nearestTimegroup = target.closest(\"ef-timegroup\");\n if (nearestTimegroup && isEFTemporal(nearestTimegroup)) {\n timeSource = nearestTimegroup as AnimatableElement;\n }\n }\n\n // For stagger offset, we need to find the actual text segment element.\n // CSS animations might be on the segment itself or on a child element.\n // If the target is not a text segment, try to find the parent text segment.\n let staggerElement: AnimatableElement = timeSource;\n if (target && target instanceof HTMLElement) {\n // Check if target is a text segment\n const targetAsAnimatable = target as AnimatableElement;\n if (supportsStaggerOffset(targetAsAnimatable)) {\n staggerElement = targetAsAnimatable;\n } else {\n // Target might be a child element - find the parent text segment\n const parentSegment = target.closest(\"ef-text-segment\");\n if (\n parentSegment &&\n supportsStaggerOffset(parentSegment as AnimatableElement)\n ) {\n staggerElement = parentSegment as AnimatableElement;\n }\n }\n }\n\n const effectiveDelay = calculateEffectiveDelay(timing.delay, staggerElement);\n mapAndSetAnimationTime(animation, timeSource, timing, effectiveDelay);\n};\n\n/**\n * Coordinates animations for a single element and its subtree, using the element as the time source.\n *\n * Uses tracked animations to ensure we can control animations even after they complete.\n * Both CSS animations (created via the 'animation' property) and WAAPI animations are included.\n *\n * CRITICAL: CSS animations are created asynchronously when classes are added. This function\n * discovers new animations on each call and tracks them in memory. Once animations complete,\n * they're removed from getAnimations(), but we keep references to them so we can continue\n * controlling them.\n */\nconst coordinateElementAnimations = (\n element: AnimatableElement,\n providedAnimations?: Animation[],\n): void => {\n // Discover and track animations (includes both current and previously completed ones)\n // Reuse the current animations array to avoid calling getAnimations() twice\n // Accept pre-discovered animations to avoid redundant getAnimations() calls\n const { tracked: trackedAnimations, current: currentAnimations } =\n discoverAndTrackAnimations(element, providedAnimations);\n\n for (const animation of trackedAnimations) {\n // Skip invalid animations (cancelled, removed from DOM, etc.)\n if (!isAnimationValid(animation, currentAnimations)) {\n continue;\n }\n\n prepareAnimation(animation);\n synchronizeAnimation(animation, element);\n }\n};\n\n// ============================================================================\n// Visual State Application\n// ============================================================================\n\n/**\n * Applies visual state (CSS + display) to match temporal state.\n *\n * WHY: This function applies visual state based on the element's phase and state.\n * Phase determines what should be visible, and this function applies that decision.\n */\nconst applyVisualState = (\n element: AnimatableElement,\n state: TemporalState,\n): void => {\n // Always set progress (needed for many use cases)\n element.style.setProperty(PROGRESS_PROPERTY, `${state.progress}`);\n\n // Handle visibility based on phase\n if (!state.isVisible) {\n element.style.setProperty(\"display\", \"none\");\n return;\n }\n element.style.removeProperty(\"display\");\n\n // Set other CSS properties for visible elements only\n element.style.setProperty(DURATION_PROPERTY, `${element.durationMs}ms`);\n element.style.setProperty(\n TRANSITION_DURATION_PROPERTY,\n `${element.parentTimegroup?.overlapMs ?? 0}ms`,\n );\n element.style.setProperty(\n TRANSITION_OUT_START_PROPERTY,\n `${element.durationMs - (element.parentTimegroup?.overlapMs ?? 0)}ms`,\n );\n};\n\n/**\n * Applies animation coordination if the element phase requires it.\n *\n * WHY: Animation coordination is driven by phase. If the element is in a phase\n * where animations should be coordinated, we coordinate them.\n */\nconst applyAnimationCoordination = (\n element: AnimatableElement,\n phase: ElementPhase,\n providedAnimations?: Animation[],\n): void => {\n if (shouldCoordinateAnimations(phase, element)) {\n coordinateElementAnimations(element, providedAnimations);\n }\n};\n\n// ============================================================================\n// SVG SMIL Synchronization\n// ============================================================================\n\n/**\n * Finds the nearest temporal ancestor (or self) for a given element.\n * Returns the element itself if it is a temporal element, otherwise walks up.\n * Falls back to the provided root element.\n */\nconst findNearestTemporalAncestor = (\n element: Element,\n root: AnimatableElement,\n): AnimatableElement => {\n let node: Element | null = element;\n while (node) {\n if (isEFTemporal(node)) {\n return node as AnimatableElement;\n }\n node = node.parentElement;\n }\n return root;\n};\n\n/**\n * Synchronizes all SVG SMIL animations in the subtree with the timeline.\n *\n * SVG has its own animation clock on each SVGSVGElement. We pause it and\n * seek it to the owning temporal element's current time (converted to seconds).\n */\nconst synchronizeSvgAnimations = (root: AnimatableElement): void => {\n const svgElements = (root as HTMLElement).querySelectorAll(\"svg\");\n for (const svg of svgElements) {\n const owner = findNearestTemporalAncestor(svg, root);\n const timeMs = owner.currentTimeMs ?? 0;\n svg.setCurrentTime(timeMs / 1000);\n svg.pauseAnimations();\n }\n};\n\n// ============================================================================\n// Media Element Synchronization\n// ============================================================================\n\n/**\n * Synchronizes all <video> and <audio> elements in the subtree with the timeline.\n *\n * Sets currentTime (in seconds) to match the owning temporal element's position\n * and ensures the elements are paused (playback is controlled by the timeline).\n */\nconst synchronizeMediaElements = (root: AnimatableElement): void => {\n const mediaElements = (root as HTMLElement).querySelectorAll(\"video, audio\");\n for (const media of mediaElements as NodeListOf<HTMLMediaElement>) {\n const owner = findNearestTemporalAncestor(media, root);\n const timeMs = owner.currentTimeMs ?? 0;\n const timeSec = timeMs / 1000;\n if (media.currentTime !== timeSec) {\n media.currentTime = timeSec;\n }\n if (!media.paused) {\n media.pause();\n }\n }\n};\n\n// ============================================================================\n// Main Function\n// ============================================================================\n\n/**\n * Evaluates the complete state for an element update.\n * This separates evaluation (what should the state be?) from application (apply that state).\n */\nconst evaluateElementState = (\n element: AnimatableElement,\n): ElementUpdateContext => {\n return {\n element,\n state: evaluateTemporalState(element),\n };\n};\n\n/**\n * Main function: synchronizes DOM element with timeline.\n *\n * Orchestrates clear flow: Phase → Policy → Time Mapping → State Application\n *\n * WHY: This function makes the conceptual flow explicit:\n * 1. Determine phase (what phase is the element in?)\n * 2. Apply policies (should it be visible/coordinated based on phase?)\n * 3. Map time for animations (transform element time to animation time)\n * 4. Apply visual state (update CSS and display based on phase and policies)\n */\nexport const updateAnimations = (element: AnimatableElement): void => {\n const allAnimations = element.getAnimations({ subtree: true });\n\n const rootContext = evaluateElementState(element);\n const timelineTimeMs = (element.rootTimegroup ?? element).currentTimeMs;\n const { elements: collectedElements, pruned } = deepGetTemporalElements(\n element,\n timelineTimeMs,\n );\n\n // For pruned elements (invisible containers whose subtrees were skipped),\n // just set display:none directly — no need to evaluate phase/state since\n // we already know they're outside their time range.\n for (const prunedElement of pruned) {\n prunedElement.style.setProperty(\"display\", \"none\");\n }\n\n // Evaluate state only for non-pruned elements (visible + individually\n // invisible leaf elements that weren't behind a pruned container).\n const childContexts: ElementUpdateContext[] = [];\n for (const temporalElement of collectedElements) {\n if (!pruned.has(temporalElement)) {\n childContexts.push(evaluateElementState(temporalElement));\n }\n }\n\n // Separate visible and invisible children.\n // Only visible children need animation coordination (expensive).\n // Invisible children just need display:none applied (cheap).\n const visibleChildContexts: ElementUpdateContext[] = [];\n for (const ctx of childContexts) {\n if (shouldBeVisible(ctx.state.phase, ctx.element)) {\n visibleChildContexts.push(ctx);\n }\n }\n\n // Partition allAnimations by closest VISIBLE temporal parent.\n // Only visible elements need their animations partitioned and coordinated.\n // Build a Set of visible temporal elements for O(1) lookup, then walk up\n // from each animation target to find its closest temporal owner.\n const temporalSet = new Set<Element>(\n visibleChildContexts.map((c) => c.element),\n );\n temporalSet.add(element); // Include root\n const childAnimations = new Map<AnimatableElement, Animation[]>();\n for (const animation of allAnimations) {\n const effect = animation.effect;\n const target =\n effect && effect instanceof KeyframeEffect ? effect.target : null;\n if (!target || !(target instanceof Element)) continue;\n\n let node: Element | null = target;\n while (node) {\n if (temporalSet.has(node)) {\n let anims = childAnimations.get(node as AnimatableElement);\n if (!anims) {\n anims = [];\n childAnimations.set(node as AnimatableElement, anims);\n }\n anims.push(animation);\n break;\n }\n node = node.parentElement;\n }\n }\n\n // Coordinate animations for root and VISIBLE children only.\n // Invisible children (display:none) have no CSS animations to coordinate,\n // and when they become visible again, coordination runs on that frame.\n applyAnimationCoordination(\n rootContext.element,\n rootContext.state.phase,\n allAnimations,\n );\n for (const context of visibleChildContexts) {\n applyAnimationCoordination(\n context.element,\n context.state.phase,\n childAnimations.get(context.element) || [],\n );\n }\n\n // Apply visual state for non-pruned children (pruned ones already got display:none above)\n applyVisualState(rootContext.element, rootContext.state);\n for (const context of childContexts) {\n applyVisualState(context.element, context.state);\n }\n\n // Synchronize non-CSS animation systems\n synchronizeSvgAnimations(element);\n synchronizeMediaElements(element);\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"updateAnimations.js","names":["warnings: string[]","timeSource: AnimatableElement","staggerElement: AnimatableElement","node: Element | null","childContexts: ElementUpdateContext[]","visibleChildContexts: ElementUpdateContext[]"],"sources":["../../src/elements/updateAnimations.ts"],"sourcesContent":["import {\n deepGetTemporalElements,\n isEFTemporal,\n type TemporalMixinInterface,\n} from \"./EFTemporal.ts\";\n\n// All animatable elements are temporal elements with HTMLElement interface\nexport type AnimatableElement = TemporalMixinInterface & HTMLElement;\n\n// ============================================================================\n// Constants\n// ============================================================================\n\nconst ANIMATION_PRECISION_OFFSET = 0.1; // Use 0.1ms to safely avoid completion threshold\nconst DEFAULT_ANIMATION_ITERATIONS = 1;\nconst PROGRESS_PROPERTY = \"--ef-progress\";\nconst DURATION_PROPERTY = \"--ef-duration\";\nconst TRANSITION_DURATION_PROPERTY = \"--ef-transition-duration\";\nconst TRANSITION_OUT_START_PROPERTY = \"--ef-transition-out-start\";\n\n// ============================================================================\n// Animation Tracking\n// ============================================================================\n\n/**\n * Tracks animations per element to prevent them from being lost when they complete.\n * Once an animation reaches 100% completion, it's removed from getAnimations(),\n * but we keep a reference to it so we can continue controlling it.\n */\nconst animationTracker = new WeakMap<Element, Set<Animation>>();\n\n/**\n * Tracks whether DOM structure has changed for an element, requiring animation rediscovery.\n * For render clones (static DOM), this stays false after initial discovery.\n * For prime timeline (interactive), this is set to true when mutations occur.\n */\nconst domStructureChanged = new WeakMap<Element, boolean>();\n\n/**\n * Tracks the last known animation count for an element to detect new animations.\n * Used as a lightweight check before calling expensive getAnimations().\n */\nconst lastAnimationCount = new WeakMap<Element, number>();\n\n/**\n * Tracks which animations have already been validated to avoid duplicate warnings.\n * Uses animation name + duration as the unique key.\n */\nconst validatedAnimations = new Set<string>();\n\n/**\n * Validates that an animation is still valid and controllable.\n * Animations become invalid when:\n * - They've been cancelled (idle state and not in getAnimations())\n * - Their effect is null (animation was removed)\n * - Their target is no longer in the DOM\n */\nconst isAnimationValid = (\n animation: Animation,\n currentAnimations: Animation[],\n): boolean => {\n // Check if animation has been cancelled\n if (\n animation.playState === \"idle\" &&\n !currentAnimations.includes(animation)\n ) {\n return false;\n }\n\n // Check if animation effect is still valid\n const effect = animation.effect;\n if (!effect) {\n return false;\n }\n\n // Check if target is still in DOM\n if (effect instanceof KeyframeEffect) {\n const target = effect.target;\n if (target && target instanceof Element) {\n if (!target.isConnected) {\n return false;\n }\n }\n }\n\n return true;\n};\n\n/**\n * Discovers and tracks animations on an element and its subtree.\n * This ensures we have references to animations even after they complete.\n *\n * Tracks animations per element where they exist, not just on the root element.\n * This allows us to find animations on any element in the subtree.\n *\n * OPTIMIZATION: For render clones (static DOM), discovery happens once at creation.\n * For prime timeline (interactive), discovery is responsive to DOM changes.\n *\n * Also cleans up invalid animations (cancelled, removed from DOM, etc.)\n *\n * @param providedAnimations - Optional pre-discovered animations to avoid redundant getAnimations() calls\n */\nconst discoverAndTrackAnimations = (\n element: AnimatableElement,\n providedAnimations?: Animation[],\n): { tracked: Set<Animation>; current: Animation[] } => {\n animationTracker.has(element);\n const structureChanged = domStructureChanged.get(element) ?? true;\n\n // REMOVED: Clone optimization that cached animation references.\n // The optimization assumed animations were \"static\" for clones, but this was incorrect.\n // After seeking to a new time, we need fresh animation state from the browser.\n // Caching caused animations to be stuck at their discovery state (often 0ms).\n\n // For prime timeline or first discovery: get current animations from the browser (includes subtree)\n // CRITICAL: This is expensive, so we return it to avoid calling it again\n // If animations were provided by caller (to avoid redundant calls), use those\n const currentAnimations =\n providedAnimations ?? element.getAnimations({ subtree: true });\n\n // Mark structure as stable after discovery\n // This prevents redundant getAnimations() calls when DOM hasn't changed\n domStructureChanged.set(element, false);\n\n // Track animation count for lightweight change detection\n lastAnimationCount.set(element, currentAnimations.length);\n\n // Track animations on each element where they exist\n for (const animation of currentAnimations) {\n const effect = animation.effect;\n const target =\n effect && effect instanceof KeyframeEffect ? effect.target : null;\n if (target && target instanceof Element) {\n let tracked = animationTracker.get(target);\n if (!tracked) {\n tracked = new Set<Animation>();\n animationTracker.set(target, tracked);\n }\n tracked.add(animation);\n }\n }\n\n // Also maintain a set on the root element for coordination\n let rootTracked = animationTracker.get(element);\n if (!rootTracked) {\n rootTracked = new Set<Animation>();\n animationTracker.set(element, rootTracked);\n }\n\n // Update root set with all current animations\n for (const animation of currentAnimations) {\n rootTracked.add(animation);\n }\n\n // Clean up invalid animations from root set\n // This handles animations that were cancelled, removed from DOM, or had their effects removed\n for (const animation of rootTracked) {\n if (!isAnimationValid(animation, currentAnimations)) {\n rootTracked.delete(animation);\n }\n }\n\n // Build a map of element -> current animations from the subtree lookup we already did\n // This avoids calling getAnimations() repeatedly on each element (expensive!)\n const elementAnimationsMap = new Map<Element, Animation[]>();\n for (const animation of currentAnimations) {\n const effect = animation.effect;\n const target =\n effect && effect instanceof KeyframeEffect ? effect.target : null;\n if (target && target instanceof Element) {\n let anims = elementAnimationsMap.get(target);\n if (!anims) {\n anims = [];\n elementAnimationsMap.set(target, anims);\n }\n anims.push(animation);\n }\n }\n\n // Clean up invalid animations from per-element sets.\n // Only walk the full subtree when DOM structure has changed (elements added/removed).\n // During scrubbing with static DOM, skip this expensive querySelectorAll(\"*\").\n if (structureChanged) {\n for (const [el, tracked] of elementAnimationsMap) {\n const existingTracked = animationTracker.get(el);\n if (existingTracked) {\n for (const animation of existingTracked) {\n if (!isAnimationValid(animation, tracked)) {\n existingTracked.delete(animation);\n }\n }\n if (existingTracked.size === 0) {\n animationTracker.delete(el);\n }\n }\n }\n }\n\n return { tracked: rootTracked, current: currentAnimations };\n};\n\n/**\n * Cleans up tracked animations when an element is disconnected.\n * This prevents memory leaks.\n */\nexport const cleanupTrackedAnimations = (element: Element): void => {\n animationTracker.delete(element);\n domStructureChanged.delete(element);\n lastAnimationCount.delete(element);\n};\n\n/**\n * Marks that DOM structure has changed for an element, requiring animation rediscovery.\n * Should be called when elements are added/removed or CSS classes change that affect animations.\n */\nexport const markDomStructureChanged = (element: Element): void => {\n domStructureChanged.set(element, true);\n};\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Represents the phase an element is in relative to the timeline.\n * This is the primary concept that drives all visibility and animation decisions.\n */\nexport type ElementPhase =\n | \"before-start\"\n | \"active\"\n | \"at-end-boundary\"\n | \"after-end\";\n\n/**\n * Represents the temporal state of an element relative to the timeline\n */\ninterface TemporalState {\n progress: number;\n isVisible: boolean;\n timelineTimeMs: number;\n phase: ElementPhase;\n}\n\n/**\n * Context object that holds all evaluated state for an element update.\n * This groups related state together, reducing parameter passing and making\n * the data flow clearer.\n */\ninterface ElementUpdateContext {\n element: AnimatableElement;\n state: TemporalState;\n}\n\n/**\n * Animation timing information extracted from an animation effect.\n * Groups related timing properties together.\n */\ninterface AnimationTiming {\n duration: number;\n delay: number;\n iterations: number;\n direction: string;\n}\n\n/**\n * Capability interface for elements that support stagger offset.\n * This encapsulates the stagger behavior behind a capability check rather than\n * leaking tag name checks throughout the codebase.\n */\ninterface StaggerableElement extends AnimatableElement {\n staggerOffsetMs?: number;\n}\n\n// ============================================================================\n// Phase Determination\n// ============================================================================\n\n/**\n * Determines what phase an element is in relative to the timeline.\n *\n * WHY: Phase is the primary concept that drives all decisions. By explicitly\n * enumerating phases, we make the code's logic clear: phase determines visibility,\n * animation coordination, and visual state.\n *\n * Phases:\n * - before-start: Timeline is before element's start time\n * - active: Timeline is within element's active range (start to end, exclusive of end)\n * - at-end-boundary: Timeline is exactly at element's end time\n * - after-end: Timeline is after element's end time\n *\n * Note: We detect \"at-end-boundary\" by checking if timeline equals end time.\n * The boundary policy will then determine if this should be treated as visible/active\n * or not based on element characteristics.\n */\nconst determineElementPhase = (\n element: AnimatableElement,\n timelineTimeMs: number,\n): ElementPhase => {\n // Read endTimeMs once to avoid recalculation issues\n const endTimeMs = element.endTimeMs;\n const startTimeMs = element.startTimeMs;\n\n // Invalid range (end <= start) means element hasn't computed its duration yet,\n // or has no temporal children (e.g., timegroup with only static HTML).\n // Treat as always active - these elements should be visible at all times.\n if (endTimeMs <= startTimeMs) {\n return \"active\";\n }\n\n if (timelineTimeMs < startTimeMs) {\n return \"before-start\";\n }\n // Use epsilon to handle floating point precision issues\n const epsilon = 0.001;\n const diff = timelineTimeMs - endTimeMs;\n\n // If clearly after end (difference > epsilon), return 'after-end'\n if (diff > epsilon) {\n return \"after-end\";\n }\n // If at or very close to end boundary (within epsilon), return 'at-end-boundary'\n if (Math.abs(diff) <= epsilon) {\n return \"at-end-boundary\";\n }\n // Otherwise, we're before the end, so check if we're active\n return \"active\";\n};\n\n// ============================================================================\n// Boundary Policies\n// ============================================================================\n\n/**\n * Policy interface for determining behavior at boundaries.\n * Different policies apply different rules for when elements should be visible\n * or have animations coordinated at exact boundary times.\n */\ninterface BoundaryPolicy {\n /**\n * Determines if an element should be considered visible/active at the end boundary\n * based on the element's characteristics.\n */\n shouldIncludeEndBoundary(element: AnimatableElement): boolean;\n}\n\n/**\n * Visibility policy: determines when elements should be visible for display purposes.\n *\n * WHY: Root elements, elements aligned with composition end, and text segments\n * should remain visible at exact end time to prevent flicker and show final frames.\n * Other elements use exclusive end for clean transitions between elements.\n */\nclass VisibilityPolicy implements BoundaryPolicy {\n shouldIncludeEndBoundary(element: AnimatableElement): boolean {\n // Root elements should remain visible at exact end time to prevent flicker\n const isRootElement = !element.parentTimegroup;\n if (isRootElement) {\n return true;\n }\n\n // Elements aligned with composition end should remain visible at exact end time\n const isLastElementInComposition =\n element.endTimeMs === element.rootTimegroup?.endTimeMs;\n if (isLastElementInComposition) {\n return true;\n }\n\n // Text segments use inclusive end since they're meant to be visible for full duration\n if (this.isTextSegment(element)) {\n return true;\n }\n\n // Other elements use exclusive end for clean transitions\n return false;\n }\n\n /**\n * Checks if element is a text segment.\n * Encapsulates the tag name check to hide implementation detail.\n */\n protected isTextSegment(element: AnimatableElement): boolean {\n return element.tagName === \"EF-TEXT-SEGMENT\";\n }\n}\n\n// Policy instances (singleton pattern for stateless policies)\nconst visibilityPolicy = new VisibilityPolicy();\n\n/**\n * Determines if an element should be visible based on its phase and visibility policy.\n */\nconst shouldBeVisible = (\n phase: ElementPhase,\n element: AnimatableElement,\n): boolean => {\n if (phase === \"before-start\" || phase === \"after-end\") {\n return false;\n }\n if (phase === \"active\") {\n return true;\n }\n // phase === \"at-end-boundary\"\n return visibilityPolicy.shouldIncludeEndBoundary(element);\n};\n\n/**\n * Determines if animations should be coordinated based on element phase and animation policy.\n *\n * CRITICAL: Always returns true to support scrubbing to arbitrary times.\n *\n * Previously, this function skipped coordination for before-start and after-end phases as an\n * optimization for live playback. However, this broke scrubbing scenarios where we seek to\n * arbitrary times (timeline scrubbing, thumbnails, video export).\n *\n * The performance cost of always coordinating is minimal:\n * - Animations only update when element time changes\n * - Paused animation updates are optimized by the browser\n * - The benefit is correct animation state at all times, regardless of phase\n */\nconst shouldCoordinateAnimations = (\n _phase: ElementPhase,\n _element: AnimatableElement,\n): boolean => {\n return true;\n};\n\n// ============================================================================\n// Temporal State Evaluation\n// ============================================================================\n\n/**\n * Evaluates what the element's state should be based on the timeline.\n *\n * WHY: This function determines the complete temporal state including phase,\n * which becomes the primary driver for all subsequent decisions.\n */\nexport const evaluateTemporalState = (\n element: AnimatableElement,\n): TemporalState => {\n // Get timeline time from root timegroup, or use element's own time if it IS a timegroup\n const timelineTimeMs = (element.rootTimegroup ?? element).currentTimeMs;\n\n const progress =\n element.durationMs <= 0\n ? 1\n : Math.max(0, Math.min(1, element.currentTimeMs / element.durationMs));\n\n const phase = determineElementPhase(element, timelineTimeMs);\n const isVisible = shouldBeVisible(phase, element);\n\n return { progress, isVisible, timelineTimeMs, phase };\n};\n\n/**\n * Evaluates element visibility state specifically for animation coordination.\n * Uses inclusive end boundaries to prevent animation jumps at exact boundaries.\n *\n * This is exported for external use cases that need animation-specific visibility\n * evaluation without the full ElementUpdateContext.\n */\nexport const evaluateAnimationVisibilityState = (\n element: AnimatableElement,\n): TemporalState => {\n const state = evaluateTemporalState(element);\n // Override visibility based on animation policy\n const shouldCoordinate = shouldCoordinateAnimations(state.phase, element);\n return { ...state, isVisible: shouldCoordinate };\n};\n\n// ============================================================================\n// Animation Time Mapping\n// ============================================================================\n\n/**\n * Capability check: determines if an element supports stagger offset.\n * Encapsulates the knowledge of which element types support this feature.\n */\nconst supportsStaggerOffset = (\n element: AnimatableElement,\n): element is StaggerableElement => {\n // Currently only text segments support stagger offset\n return element.tagName === \"EF-TEXT-SEGMENT\";\n};\n\n/**\n * Calculates effective delay including stagger offset if applicable.\n *\n * Stagger offset allows elements (like text segments) to have their animations\n * start at different times while keeping their visibility timing unchanged.\n * This enables staggered animation effects within a single timegroup.\n */\nconst calculateEffectiveDelay = (\n delay: number,\n element: AnimatableElement,\n): number => {\n if (supportsStaggerOffset(element)) {\n // Read stagger offset - try property first (more reliable), then CSS variable\n // The staggerOffsetMs property is set directly on the element and is always available\n const segment = element as any;\n if (\n segment.staggerOffsetMs !== undefined &&\n segment.staggerOffsetMs !== null\n ) {\n return delay + segment.staggerOffsetMs;\n }\n\n // Fallback to CSS variable if property not available\n let cssValue = (element as HTMLElement).style\n .getPropertyValue(\"--ef-stagger-offset\")\n .trim();\n\n if (!cssValue) {\n cssValue = window\n .getComputedStyle(element)\n .getPropertyValue(\"--ef-stagger-offset\")\n .trim();\n }\n\n if (cssValue) {\n // Parse \"100ms\" format to milliseconds\n const match = cssValue.match(/(\\d+(?:\\.\\d+)?)\\s*ms?/);\n if (match) {\n const staggerOffset = parseFloat(match[1]!);\n if (!isNaN(staggerOffset)) {\n return delay + staggerOffset;\n }\n } else {\n // Try parsing as just a number\n const numValue = parseFloat(cssValue);\n if (!isNaN(numValue)) {\n return delay + numValue;\n }\n }\n }\n }\n return delay;\n};\n\n/**\n * Calculates maximum safe animation time to prevent completion.\n *\n * WHY: Once an animation reaches \"finished\" state, it can no longer be manually controlled\n * via currentTime. By clamping to just before completion (using ANIMATION_PRECISION_OFFSET),\n * we ensure the animation remains in a controllable state, allowing us to synchronize it\n * with the timeline even when it would naturally be complete.\n */\nconst calculateMaxSafeAnimationTime = (\n duration: number,\n iterations: number,\n): number => {\n return duration * iterations - ANIMATION_PRECISION_OFFSET;\n};\n\n/**\n * Determines if the current iteration should be reversed based on direction\n */\nconst shouldReverseIteration = (\n direction: string,\n currentIteration: number,\n): boolean => {\n return (\n direction === \"reverse\" ||\n (direction === \"alternate\" && currentIteration % 2 === 1) ||\n (direction === \"alternate-reverse\" && currentIteration % 2 === 0)\n );\n};\n\n/**\n * Applies direction to iteration time (reverses if needed)\n */\nconst applyDirectionToIterationTime = (\n currentIterationTime: number,\n duration: number,\n direction: string,\n currentIteration: number,\n): number => {\n if (shouldReverseIteration(direction, currentIteration)) {\n return duration - currentIterationTime;\n }\n return currentIterationTime;\n};\n\n/**\n * Maps element time to animation time for normal direction.\n * Uses cumulative time throughout the animation.\n * Note: elementTime should already be adjusted (elementTime - effectiveDelay).\n */\nconst mapNormalDirectionTime = (\n elementTime: number,\n duration: number,\n maxSafeTime: number,\n): number => {\n const currentIteration = Math.floor(elementTime / duration);\n const iterationTime = elementTime % duration;\n const cumulativeTime = currentIteration * duration + iterationTime;\n return Math.min(cumulativeTime, maxSafeTime);\n};\n\n/**\n * Maps element time to animation time for reverse direction.\n * Uses cumulative time with reversed iterations.\n * Note: elementTime should already be adjusted (elementTime - effectiveDelay).\n */\nconst mapReverseDirectionTime = (\n elementTime: number,\n duration: number,\n maxSafeTime: number,\n): number => {\n const currentIteration = Math.floor(elementTime / duration);\n const rawIterationTime = elementTime % duration;\n const reversedIterationTime = duration - rawIterationTime;\n const cumulativeTime = currentIteration * duration + reversedIterationTime;\n return Math.min(cumulativeTime, maxSafeTime);\n};\n\n/**\n * Maps element time to animation time for alternate/alternate-reverse directions.\n *\n * WHY SPECIAL HANDLING: Alternate directions oscillate between forward and reverse iterations.\n * Without delay, we use iteration time (0 to duration) because the animation naturally\n * resets each iteration. However, with delay, iteration 0 needs to account for the delay\n * offset (using ownCurrentTimeMs), and later iterations need cumulative time to properly\n * track progress across multiple iterations. This complexity requires a dedicated mapper\n * rather than trying to handle it in the general case.\n */\nconst mapAlternateDirectionTime = (\n elementTime: number,\n effectiveDelay: number,\n duration: number,\n direction: string,\n maxSafeTime: number,\n): number => {\n const adjustedTime = elementTime - effectiveDelay;\n\n if (effectiveDelay > 0) {\n // With delay: iteration 0 uses elementTime to include delay offset,\n // later iterations use cumulative time to track progress across iterations\n const currentIteration = Math.floor(adjustedTime / duration);\n if (currentIteration === 0) {\n return Math.min(elementTime, maxSafeTime);\n }\n return Math.min(adjustedTime, maxSafeTime);\n }\n\n // Without delay: use iteration time (after direction applied) since animation\n // naturally resets each iteration\n const currentIteration = Math.floor(elementTime / duration);\n const rawIterationTime = elementTime % duration;\n const iterationTime = applyDirectionToIterationTime(\n rawIterationTime,\n duration,\n direction,\n currentIteration,\n );\n return Math.min(iterationTime, maxSafeTime);\n};\n\n/**\n * Maps element time to animation time based on direction.\n *\n * WHY: This function explicitly transforms element time to animation time, making\n * the time mapping concept clear. Different directions require different transformations\n * to achieve the desired visual effect.\n */\nconst mapElementTimeToAnimationTime = (\n elementTime: number,\n timing: AnimationTiming,\n effectiveDelay: number,\n): number => {\n const { duration, iterations, direction } = timing;\n const maxSafeTime = calculateMaxSafeAnimationTime(duration, iterations);\n // Calculate adjusted time (element time minus delay) for normal/reverse directions\n const adjustedTime = elementTime - effectiveDelay;\n\n if (direction === \"reverse\") {\n return mapReverseDirectionTime(adjustedTime, duration, maxSafeTime);\n }\n if (direction === \"alternate\" || direction === \"alternate-reverse\") {\n return mapAlternateDirectionTime(\n elementTime,\n effectiveDelay,\n duration,\n direction,\n maxSafeTime,\n );\n }\n // normal direction - use adjustedTime to account for delay\n return mapNormalDirectionTime(adjustedTime, duration, maxSafeTime);\n};\n\n/**\n * Determines the animation time for a completed animation based on direction.\n */\nconst getCompletedAnimationTime = (\n timing: AnimationTiming,\n maxSafeTime: number,\n): number => {\n const { direction, iterations, duration } = timing;\n\n if (direction === \"alternate\" || direction === \"alternate-reverse\") {\n // For alternate directions, determine if final iteration is reversed\n const finalIteration = iterations - 1;\n const isFinalIterationReversed =\n (direction === \"alternate\" && finalIteration % 2 === 1) ||\n (direction === \"alternate-reverse\" && finalIteration % 2 === 0);\n\n if (isFinalIterationReversed) {\n // At end of reversed iteration, currentTime should be near 0 (but clamped)\n return Math.min(duration - ANIMATION_PRECISION_OFFSET, maxSafeTime);\n }\n }\n\n // For normal, reverse, or forward final iteration of alternate: use max safe time\n return maxSafeTime;\n};\n\n/**\n * Validates that animation effect is a KeyframeEffect with a target\n */\nconst validateAnimationEffect = (\n effect: AnimationEffect | null,\n): effect is KeyframeEffect & { target: Element } => {\n return (\n effect !== null &&\n effect instanceof KeyframeEffect &&\n effect.target !== null\n );\n};\n\n/**\n * Extracts timing information from an animation effect.\n * Duration and delay from getTiming() are already in milliseconds.\n * We use getTiming().delay directly from the animation object.\n */\nconst extractAnimationTiming = (effect: KeyframeEffect): AnimationTiming => {\n const timing = effect.getTiming();\n\n return {\n duration: Number(timing.duration) || 0,\n delay: Number(timing.delay) || 0,\n iterations: Number(timing.iterations) || DEFAULT_ANIMATION_ITERATIONS,\n direction: timing.direction || \"normal\",\n };\n};\n\n// ============================================================================\n// Animation Fill Mode Validation (Development Mode)\n// ============================================================================\n\n/**\n * Analyzes keyframes to detect if animation is a fade-in or fade-out effect.\n * Returns 'fade-in', 'fade-out', 'both', or null.\n */\nconst detectFadePattern = (\n keyframes: Keyframe[],\n): \"fade-in\" | \"fade-out\" | \"both\" | null => {\n if (!keyframes || keyframes.length < 2) return null;\n\n const firstFrame = keyframes[0];\n const lastFrame = keyframes[keyframes.length - 1];\n\n const firstOpacity =\n firstFrame && \"opacity\" in firstFrame ? Number(firstFrame.opacity) : null;\n const lastOpacity =\n lastFrame && \"opacity\" in lastFrame ? Number(lastFrame.opacity) : null;\n\n if (firstOpacity === null || lastOpacity === null) return null;\n\n const isFadeIn = firstOpacity < lastOpacity;\n const isFadeOut = firstOpacity > lastOpacity;\n\n if (isFadeIn && isFadeOut) return \"both\";\n if (isFadeIn) return \"fade-in\";\n if (isFadeOut) return \"fade-out\";\n return null;\n};\n\n/**\n * Analyzes keyframes to detect if animation has transform changes (slide, scale, etc).\n */\nconst hasTransformAnimation = (keyframes: Keyframe[]): boolean => {\n if (!keyframes || keyframes.length < 2) return false;\n\n return keyframes.some(\n (frame) =>\n \"transform\" in frame ||\n \"translate\" in frame ||\n \"scale\" in frame ||\n \"rotate\" in frame,\n );\n};\n\n/**\n * Validates CSS animation fill-mode to prevent flashing issues.\n *\n * CRITICAL: Editframe's timeline system pauses animations and manually controls them\n * via animation.currentTime. This means elements exist in the DOM before their animations\n * start. Without proper fill-mode, elements will \"flash\" to their natural state before\n * the animation begins.\n *\n * Common issues:\n * - Delayed animations without 'backwards': Element shows natural state during delay\n * - Fade-in without 'backwards': Element visible before fade starts\n * - Fade-out without 'forwards': Element snaps back after fade completes\n *\n * Only runs in development mode to avoid performance impact in production.\n */\nconst validateAnimationFillMode = (\n animation: Animation,\n timing: AnimationTiming,\n): void => {\n // Only validate in development mode\n if (\n typeof process !== \"undefined\" &&\n process.env?.NODE_ENV === \"production\"\n ) {\n return;\n }\n\n const effect = animation.effect;\n if (!validateAnimationEffect(effect)) {\n return;\n }\n\n const effectTiming = effect.getTiming();\n const fill = effectTiming.fill || \"none\";\n const target = effect.target;\n\n // Get animation name for better error messages\n let animationName = \"unknown\";\n if (animation.id) {\n animationName = animation.id;\n } else if (target instanceof HTMLElement) {\n const computedStyle = window.getComputedStyle(target);\n const animationNameValue = computedStyle.animationName;\n if (animationNameValue && animationNameValue !== \"none\") {\n animationName = animationNameValue.split(\",\")[0]?.trim() || \"unknown\";\n }\n }\n\n // Create unique key based on animation name and duration\n const validationKey = `${animationName}-${timing.duration}`;\n\n // Skip if already validated\n if (validatedAnimations.has(validationKey)) {\n return;\n }\n validatedAnimations.add(validationKey);\n\n const warnings: string[] = [];\n\n // Check 1: Delayed animations without backwards/both\n if (timing.delay > 0 && fill !== \"backwards\" && fill !== \"both\") {\n warnings.push(\n `⚠️ Animation \"${animationName}\" has a ${timing.delay}ms delay but no 'backwards' fill-mode.`,\n ` This will cause the element to show its natural state during the delay, then suddenly jump when the animation starts.`,\n ` Fix: Add 'backwards' or 'both' to the animation shorthand.`,\n ` Example: animation: ${animationName} ${timing.duration}ms ${timing.delay}ms backwards;`,\n );\n }\n\n // Check 2: Analyze keyframes for fade/transform patterns\n try {\n const keyframes = effect.getKeyframes();\n const fadePattern = detectFadePattern(keyframes);\n const hasTransform = hasTransformAnimation(keyframes);\n\n // Fade-in or transform-in animations should use backwards\n if (\n (fadePattern === \"fade-in\" || hasTransform) &&\n fill !== \"backwards\" &&\n fill !== \"both\"\n ) {\n warnings.push(\n `⚠️ Animation \"${animationName}\" modifies initial state but lacks 'backwards' fill-mode.`,\n ` The element will be visible in its natural state before the animation starts.`,\n ` Fix: Add 'backwards' or 'both' to the animation.`,\n ` Example: animation: ${animationName} ${timing.duration}ms backwards;`,\n );\n }\n\n // Fade-out animations should use forwards\n if (fadePattern === \"fade-out\" && fill !== \"forwards\" && fill !== \"both\") {\n warnings.push(\n `⚠️ Animation \"${animationName}\" modifies final state but lacks 'forwards' fill-mode.`,\n ` The element will snap back to its natural state after the animation completes.`,\n ` Fix: Add 'forwards' or 'both' to the animation.`,\n ` Example: animation: ${animationName} ${timing.duration}ms forwards;`,\n );\n }\n\n // Combined effects should use both\n if (fadePattern === \"both\" && fill !== \"both\") {\n warnings.push(\n `⚠️ Animation \"${animationName}\" modifies both initial and final state but doesn't use 'both' fill-mode.`,\n ` Fix: Use 'both' to apply initial and final states.`,\n ` Example: animation: ${animationName} ${timing.duration}ms both;`,\n );\n }\n } catch (e) {\n // Silently skip keyframe analysis if it fails\n }\n\n if (warnings.length > 0 && typeof window !== \"undefined\") {\n console.groupCollapsed(\n \"%c🎬 Editframe Animation Fill-Mode Warning\",\n \"color: #f59e0b; font-weight: bold\",\n );\n warnings.forEach((warning) => console.log(warning));\n console.log(\n \"\\n📚 Learn more: https://developer.mozilla.org/en-US/docs/Web/CSS/animation-fill-mode\",\n );\n console.groupEnd();\n }\n};\n\n/**\n * Prepares animation for manual control by ensuring it's paused\n */\nconst prepareAnimation = (animation: Animation): void => {\n // Ensure animation is in a controllable state\n // Finished animations can't be controlled, so reset them\n if (animation.playState === \"finished\") {\n animation.cancel();\n // After cancel, animation is in idle state - we can set currentTime directly\n // No need to play/pause - we'll control it via currentTime\n } else if (animation.playState === \"running\") {\n // Pause running animations so we can control them manually\n animation.pause();\n }\n // For \"idle\" or \"paused\" state, we can set currentTime directly without play/pause\n // Setting currentTime on a paused animation will apply the keyframes\n // No initialization needed - we control everything via currentTime\n};\n\n/**\n * Maps element time to animation currentTime and sets it on the animation.\n *\n * WHY: This function explicitly performs the time mapping transformation,\n * making it clear that we're transforming element time to animation time.\n */\nconst mapAndSetAnimationTime = (\n animation: Animation,\n element: AnimatableElement,\n timing: AnimationTiming,\n effectiveDelay: number,\n): void => {\n // Use ownCurrentTimeMs for all elements (timegroups and other temporal elements)\n // This gives us time relative to when the element started, which ensures animations\n // on child elements are synchronized with their containing timegroup's timeline.\n // For timegroups, ownCurrentTimeMs is the time relative to when the timegroup started.\n // For other temporal elements, ownCurrentTimeMs is the time relative to their start.\n const elementTime = element.ownCurrentTimeMs ?? 0;\n\n // Ensure animation is paused before setting currentTime\n if (animation.playState === \"running\") {\n animation.pause();\n }\n\n // Calculate adjusted time (element time minus delay)\n const adjustedTime = elementTime - effectiveDelay;\n\n // If before delay, show initial keyframe state (0% of animation)\n if (adjustedTime < 0) {\n // Before delay: show initial keyframe state.\n // For CSS animations with delay > 0, currentTime is in \"absolute timeline time\"\n // (delay period + animation progress). Subtracting the stagger offset shifts\n // each segment's delay window so they enter their animation at different times.\n if (timing.delay > 0) {\n animation.currentTime = elementTime - (effectiveDelay - timing.delay);\n } else {\n animation.currentTime = 0;\n }\n return;\n }\n\n // At delay time (adjustedTime = 0) or after, the animation should be active\n const { duration, iterations } = timing;\n const currentIteration = Math.floor(adjustedTime / duration);\n\n if (currentIteration >= iterations) {\n // Animation is completed - use completed time mapping\n const maxSafeTime = calculateMaxSafeAnimationTime(duration, iterations);\n const completedAnimationTime = getCompletedAnimationTime(\n timing,\n maxSafeTime,\n );\n\n // CRITICAL: For CSS animations, currentTime behavior differs based on whether delay > 0:\n // - If timing.delay > 0: currentTime includes the delay (absolute timeline time)\n // - If timing.delay === 0: currentTime is just animation progress (0 to duration)\n if (timing.delay > 0) {\n // Completed: anchor to timing.delay (not effectiveDelay) so all segments land at\n // the same final keyframe regardless of their individual stagger offsets.\n animation.currentTime = timing.delay + completedAnimationTime;\n } else {\n // Completed: currentTime should be just the completed animation time (animation progress)\n animation.currentTime = completedAnimationTime;\n }\n } else {\n // Animation is in progress - map element time to animation time\n const animationTime = mapElementTimeToAnimationTime(\n elementTime,\n timing,\n effectiveDelay,\n );\n\n // CRITICAL: For CSS animations, currentTime behavior differs based on whether delay > 0:\n // - If timing.delay > 0: currentTime includes the delay (absolute timeline time)\n // - If timing.delay === 0: currentTime is just animation progress (0 to duration)\n // Stagger offset is handled via adjustedTime calculation, but doesn't affect currentTime format\n const { direction, delay } = timing;\n\n if (delay > 0) {\n // CSS animation with delay: currentTime is in \"absolute timeline time\" (delay + progress).\n // Anchor to timing.delay (the CSS base delay) rather than effectiveDelay so that the\n // stagger offset shifts each segment's window without cancelling out.\n const staggerShift = effectiveDelay - timing.delay;\n const isAlternateWithDelay =\n (direction === \"alternate\" || direction === \"alternate-reverse\") &&\n effectiveDelay > 0;\n if (isAlternateWithDelay && currentIteration === 0) {\n animation.currentTime = elementTime - staggerShift;\n } else {\n animation.currentTime = timing.delay + animationTime;\n }\n } else {\n // CSS animation with delay = 0: currentTime is just animation progress\n // Stagger offset is already accounted for in adjustedTime, so animationTime is the progress\n animation.currentTime = animationTime;\n }\n }\n};\n\n/**\n * Synchronizes a single animation with the timeline using the element as the time source.\n *\n * For animations in this element's subtree, always use this element as the time source.\n * This handles both animations directly on the temporal element and on its non-temporal children.\n */\nconst synchronizeAnimation = (\n animation: Animation,\n element: AnimatableElement,\n): void => {\n const effect = animation.effect;\n if (!validateAnimationEffect(effect)) {\n return;\n }\n\n const timing = extractAnimationTiming(effect);\n\n if (timing.duration <= 0) {\n animation.currentTime = 0;\n return;\n }\n\n // Validate fill-mode in development mode\n validateAnimationFillMode(animation, timing);\n\n // Find the containing timegroup for the animation target.\n // Temporal elements are always synced to timegroups, so animations should use\n // the timegroup's timeline as the time source.\n const target = effect.target;\n let timeSource: AnimatableElement = element;\n\n if (target && target instanceof HTMLElement) {\n // Find the nearest timegroup in the DOM tree\n const nearestTimegroup = target.closest(\"ef-timegroup\");\n if (nearestTimegroup && isEFTemporal(nearestTimegroup)) {\n timeSource = nearestTimegroup as AnimatableElement;\n }\n }\n\n // For stagger offset, we need to find the actual text segment element.\n // CSS animations might be on the segment itself or on a child element.\n // If the target is not a text segment, try to find the parent text segment.\n let staggerElement: AnimatableElement = timeSource;\n if (target && target instanceof HTMLElement) {\n // Check if target is a text segment\n const targetAsAnimatable = target as AnimatableElement;\n if (supportsStaggerOffset(targetAsAnimatable)) {\n staggerElement = targetAsAnimatable;\n } else {\n // Target might be a child element - find the parent text segment\n const parentSegment = target.closest(\"ef-text-segment\");\n if (\n parentSegment &&\n supportsStaggerOffset(parentSegment as AnimatableElement)\n ) {\n staggerElement = parentSegment as AnimatableElement;\n }\n }\n }\n\n const effectiveDelay = calculateEffectiveDelay(timing.delay, staggerElement);\n mapAndSetAnimationTime(animation, timeSource, timing, effectiveDelay);\n};\n\n/**\n * Coordinates animations for a single element and its subtree, using the element as the time source.\n *\n * Uses tracked animations to ensure we can control animations even after they complete.\n * Both CSS animations (created via the 'animation' property) and WAAPI animations are included.\n *\n * CRITICAL: CSS animations are created asynchronously when classes are added. This function\n * discovers new animations on each call and tracks them in memory. Once animations complete,\n * they're removed from getAnimations(), but we keep references to them so we can continue\n * controlling them.\n */\nconst coordinateElementAnimations = (\n element: AnimatableElement,\n providedAnimations?: Animation[],\n): void => {\n // Discover and track animations (includes both current and previously completed ones)\n // Reuse the current animations array to avoid calling getAnimations() twice\n // Accept pre-discovered animations to avoid redundant getAnimations() calls\n const { tracked: trackedAnimations, current: currentAnimations } =\n discoverAndTrackAnimations(element, providedAnimations);\n\n for (const animation of trackedAnimations) {\n // Skip invalid animations (cancelled, removed from DOM, etc.)\n if (!isAnimationValid(animation, currentAnimations)) {\n continue;\n }\n\n prepareAnimation(animation);\n synchronizeAnimation(animation, element);\n }\n};\n\n// ============================================================================\n// Visual State Application\n// ============================================================================\n\n/**\n * Applies visual state (CSS + display) to match temporal state.\n *\n * WHY: This function applies visual state based on the element's phase and state.\n * Phase determines what should be visible, and this function applies that decision.\n */\nconst applyVisualState = (\n element: AnimatableElement,\n state: TemporalState,\n): void => {\n // Always set progress (needed for many use cases)\n element.style.setProperty(PROGRESS_PROPERTY, `${state.progress}`);\n\n // Handle visibility based on phase\n if (!state.isVisible) {\n element.style.setProperty(\"display\", \"none\");\n return;\n }\n element.style.removeProperty(\"display\");\n\n // Set other CSS properties for visible elements only\n element.style.setProperty(DURATION_PROPERTY, `${element.durationMs}ms`);\n element.style.setProperty(\n TRANSITION_DURATION_PROPERTY,\n `${element.parentTimegroup?.overlapMs ?? 0}ms`,\n );\n element.style.setProperty(\n TRANSITION_OUT_START_PROPERTY,\n `${element.durationMs - (element.parentTimegroup?.overlapMs ?? 0)}ms`,\n );\n};\n\n/**\n * Applies animation coordination if the element phase requires it.\n *\n * WHY: Animation coordination is driven by phase. If the element is in a phase\n * where animations should be coordinated, we coordinate them.\n */\nconst applyAnimationCoordination = (\n element: AnimatableElement,\n phase: ElementPhase,\n providedAnimations?: Animation[],\n): void => {\n if (shouldCoordinateAnimations(phase, element)) {\n coordinateElementAnimations(element, providedAnimations);\n }\n};\n\n// ============================================================================\n// SVG SMIL Synchronization\n// ============================================================================\n\n/**\n * Finds the nearest temporal ancestor (or self) for a given element.\n * Returns the element itself if it is a temporal element, otherwise walks up.\n * Falls back to the provided root element.\n */\nconst findNearestTemporalAncestor = (\n element: Element,\n root: AnimatableElement,\n): AnimatableElement => {\n let node: Element | null = element;\n while (node) {\n if (isEFTemporal(node)) {\n return node as AnimatableElement;\n }\n node = node.parentElement;\n }\n return root;\n};\n\n/**\n * Synchronizes all SVG SMIL animations in the subtree with the timeline.\n *\n * SVG has its own animation clock on each SVGSVGElement. We pause it and\n * seek it to the owning temporal element's current time (converted to seconds).\n */\nconst synchronizeSvgAnimations = (root: AnimatableElement): void => {\n const svgElements = (root as HTMLElement).querySelectorAll(\"svg\");\n for (const svg of svgElements) {\n const owner = findNearestTemporalAncestor(svg, root);\n const timeMs = owner.currentTimeMs ?? 0;\n svg.setCurrentTime(timeMs / 1000);\n svg.pauseAnimations();\n }\n};\n\n// ============================================================================\n// Media Element Synchronization\n// ============================================================================\n\n/**\n * Synchronizes all <video> and <audio> elements in the subtree with the timeline.\n *\n * Sets currentTime (in seconds) to match the owning temporal element's position\n * and ensures the elements are paused (playback is controlled by the timeline).\n */\nconst synchronizeMediaElements = (root: AnimatableElement): void => {\n const mediaElements = (root as HTMLElement).querySelectorAll(\"video, audio\");\n for (const media of mediaElements as NodeListOf<HTMLMediaElement>) {\n const owner = findNearestTemporalAncestor(media, root);\n const timeMs = owner.currentTimeMs ?? 0;\n const timeSec = timeMs / 1000;\n if (media.currentTime !== timeSec) {\n media.currentTime = timeSec;\n }\n if (!media.paused) {\n media.pause();\n }\n }\n};\n\n// ============================================================================\n// Main Function\n// ============================================================================\n\n/**\n * Evaluates the complete state for an element update.\n * This separates evaluation (what should the state be?) from application (apply that state).\n */\nconst evaluateElementState = (\n element: AnimatableElement,\n): ElementUpdateContext => {\n return {\n element,\n state: evaluateTemporalState(element),\n };\n};\n\n/**\n * Main function: synchronizes DOM element with timeline.\n *\n * Orchestrates clear flow: Phase → Policy → Time Mapping → State Application\n *\n * WHY: This function makes the conceptual flow explicit:\n * 1. Determine phase (what phase is the element in?)\n * 2. Apply policies (should it be visible/coordinated based on phase?)\n * 3. Map time for animations (transform element time to animation time)\n * 4. Apply visual state (update CSS and display based on phase and policies)\n */\nexport const updateAnimations = (element: AnimatableElement): void => {\n const allAnimations = element.getAnimations({ subtree: true });\n\n const rootContext = evaluateElementState(element);\n const timelineTimeMs = (element.rootTimegroup ?? element).currentTimeMs;\n const { elements: collectedElements, pruned } = deepGetTemporalElements(\n element,\n timelineTimeMs,\n );\n\n // For pruned elements (invisible containers whose subtrees were skipped),\n // just set display:none directly — no need to evaluate phase/state since\n // we already know they're outside their time range.\n for (const prunedElement of pruned) {\n prunedElement.style.setProperty(\"display\", \"none\");\n }\n\n // Evaluate state only for non-pruned elements (visible + individually\n // invisible leaf elements that weren't behind a pruned container).\n const childContexts: ElementUpdateContext[] = [];\n for (const temporalElement of collectedElements) {\n if (!pruned.has(temporalElement)) {\n childContexts.push(evaluateElementState(temporalElement));\n }\n }\n\n // Separate visible and invisible children.\n // Only visible children need animation coordination (expensive).\n // Invisible children just need display:none applied (cheap).\n const visibleChildContexts: ElementUpdateContext[] = [];\n for (const ctx of childContexts) {\n if (shouldBeVisible(ctx.state.phase, ctx.element)) {\n visibleChildContexts.push(ctx);\n }\n }\n\n // Partition allAnimations by closest VISIBLE temporal parent.\n // Only visible elements need their animations partitioned and coordinated.\n // Build a Set of visible temporal elements for O(1) lookup, then walk up\n // from each animation target to find its closest temporal owner.\n const temporalSet = new Set<Element>(\n visibleChildContexts.map((c) => c.element),\n );\n temporalSet.add(element); // Include root\n const childAnimations = new Map<AnimatableElement, Animation[]>();\n for (const animation of allAnimations) {\n const effect = animation.effect;\n const target =\n effect && effect instanceof KeyframeEffect ? effect.target : null;\n if (!target || !(target instanceof Element)) continue;\n\n let node: Element | null = target;\n while (node) {\n if (temporalSet.has(node)) {\n let anims = childAnimations.get(node as AnimatableElement);\n if (!anims) {\n anims = [];\n childAnimations.set(node as AnimatableElement, anims);\n }\n anims.push(animation);\n break;\n }\n node = node.parentElement;\n }\n }\n\n // Coordinate animations for root and VISIBLE children only.\n // Invisible children (display:none) have no CSS animations to coordinate,\n // and when they become visible again, coordination runs on that frame.\n applyAnimationCoordination(\n rootContext.element,\n rootContext.state.phase,\n allAnimations,\n );\n for (const context of visibleChildContexts) {\n applyAnimationCoordination(\n context.element,\n context.state.phase,\n childAnimations.get(context.element) || [],\n );\n }\n\n // Apply visual state for non-pruned children (pruned ones already got display:none above)\n applyVisualState(rootContext.element, rootContext.state);\n for (const context of childContexts) {\n applyVisualState(context.element, context.state);\n }\n\n // Synchronize non-CSS animation systems\n synchronizeSvgAnimations(element);\n synchronizeMediaElements(element);\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.42.
|
|
3
|
+
"version": "0.42.5",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"license": "UNLICENSED",
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@bramus/style-observer": "^1.3.0",
|
|
21
|
-
"@editframe/assets": "0.42.
|
|
21
|
+
"@editframe/assets": "0.42.5",
|
|
22
22
|
"@lit/context": "^1.1.6",
|
|
23
23
|
"@opentelemetry/api": "^1.9.0",
|
|
24
24
|
"@opentelemetry/context-zone": "^1.26.0",
|