@editframe/elements 0.46.1 → 0.46.4

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.
@@ -30,6 +30,14 @@ const lastAnimationCount = /* @__PURE__ */ new WeakMap();
30
30
  */
31
31
  const validatedAnimations = /* @__PURE__ */ new Set();
32
32
  /**
33
+ * Tracks animations that have already been taken under manual control.
34
+ * Once an animation is here, its playState is known to be "paused" or "idle" —
35
+ * both accept currentTime writes without preconditions and without causing reflow.
36
+ * This lets prepareAnimation skip cancel()/pause() on every subsequent frame.
37
+ */
38
+ const preparedAnimations = /* @__PURE__ */ new WeakSet();
39
+ const animationCache = /* @__PURE__ */ new WeakMap();
40
+ /**
33
41
  * Validates that an animation is still valid and controllable.
34
42
  * Animations become invalid when:
35
43
  * - They've been cancelled (idle state and not in getAnimations())
@@ -37,7 +45,7 @@ const validatedAnimations = /* @__PURE__ */ new Set();
37
45
  * - Their target is no longer in the DOM
38
46
  */
39
47
  const isAnimationValid = (animation, currentAnimations) => {
40
- if (animation.playState === "idle" && !currentAnimations.includes(animation)) return false;
48
+ if (!currentAnimations.includes(animation)) return false;
41
49
  const effect = animation.effect;
42
50
  if (!effect) return false;
43
51
  if (effect instanceof KeyframeEffect) {
@@ -113,6 +121,28 @@ const discoverAndTrackAnimations = (element, providedAnimations) => {
113
121
  };
114
122
  };
115
123
  /**
124
+ * Cancels all tracked animations for an element and removes them from tracking.
125
+ * Called when an element is hidden so paused WAAPI animations leave getAnimations(),
126
+ * preventing unbounded growth of getAnimations({subtree:true}) during scrubbing.
127
+ */
128
+ const cancelTrackedAnimations = (element) => {
129
+ const tracked = animationTracker.get(element);
130
+ if (tracked) {
131
+ for (const animation of tracked) {
132
+ animation.cancel();
133
+ preparedAnimations.delete(animation);
134
+ animationCache.delete(animation);
135
+ }
136
+ tracked.clear();
137
+ }
138
+ const subtreeAnims = element.getAnimations?.({ subtree: true });
139
+ if (subtreeAnims) for (const animation of subtreeAnims) {
140
+ animation.cancel();
141
+ preparedAnimations.delete(animation);
142
+ animationCache.delete(animation);
143
+ }
144
+ };
145
+ /**
116
146
  * Cleans up tracked animations when an element is disconnected.
117
147
  * This prevents memory leaks.
118
148
  */
@@ -431,11 +461,18 @@ const validateAnimationFillMode = (animation, timing) => {
431
461
  }
432
462
  };
433
463
  /**
434
- * Prepares animation for manual control by ensuring it's paused
464
+ * Prepares animation for manual control on first encounter.
465
+ *
466
+ * Reading animation.playState forces style recalculation in Chromium (layout thrash).
467
+ * Instead we track prepared animations in a WeakSet. On first encounter we optimistically
468
+ * cancel the animation — cancel() is safe on any state (paused/running/finished/idle)
469
+ * and leaves the animation in "idle", from which currentTime writes work freely.
470
+ * On subsequent frames the animation is already under our control so we skip this entirely.
435
471
  */
436
472
  const prepareAnimation = (animation) => {
437
- if (animation.playState === "finished") animation.cancel();
438
- else if (animation.playState === "running") animation.pause();
473
+ if (preparedAnimations.has(animation)) return;
474
+ animation.cancel();
475
+ preparedAnimations.add(animation);
439
476
  };
440
477
  /**
441
478
  * Maps element time to animation currentTime and sets it on the animation.
@@ -445,7 +482,6 @@ const prepareAnimation = (animation) => {
445
482
  */
446
483
  const mapAndSetAnimationTime = (animation, element, timing, effectiveDelay) => {
447
484
  const elementTime = element.ownCurrentTimeMs ?? 0;
448
- if (animation.playState === "running") animation.pause();
449
485
  const adjustedTime = elementTime - effectiveDelay;
450
486
  if (adjustedTime < 0) {
451
487
  if (timing.delay > 0) animation.currentTime = elementTime - (effectiveDelay - timing.delay);
@@ -469,28 +505,19 @@ const mapAndSetAnimationTime = (animation, element, timing, effectiveDelay) => {
469
505
  }
470
506
  };
471
507
  /**
472
- * Synchronizes a single animation with the timeline using the element as the time source.
473
- *
474
- * For animations in this element's subtree, always use this element as the time source.
475
- * This handles both animations directly on the temporal element and on its non-temporal children.
508
+ * Builds and caches per-animation data derived from immutable properties.
509
+ * Called once per animation on first synchronization; subsequent frames use the cache.
476
510
  */
477
- const synchronizeAnimation = (animation, element) => {
478
- const effect = animation.effect;
479
- if (!validateAnimationEffect(effect)) return;
511
+ const buildAnimationCache = (animation, effect, fallbackElement) => {
480
512
  const timing = extractAnimationTiming(effect);
481
- if (timing.duration <= 0) {
482
- animation.currentTime = 0;
483
- return;
484
- }
485
- validateAnimationFillMode(animation, timing);
486
513
  const target = effect.target;
487
- let timeSource = element;
488
- if (target && target instanceof HTMLElement) {
514
+ let timeSource = null;
515
+ if (target instanceof HTMLElement) {
489
516
  const nearestTimegroup = target.closest("ef-timegroup");
490
517
  if (nearestTimegroup && isEFTemporal(nearestTimegroup)) timeSource = nearestTimegroup;
491
518
  }
492
- let staggerElement = timeSource;
493
- if (target && target instanceof HTMLElement) {
519
+ let staggerElement = null;
520
+ if (target instanceof HTMLElement) {
494
521
  const targetAsAnimatable = target;
495
522
  if (supportsStaggerOffset(targetAsAnimatable)) staggerElement = targetAsAnimatable;
496
523
  else {
@@ -498,8 +525,36 @@ const synchronizeAnimation = (animation, element) => {
498
525
  if (parentSegment && supportsStaggerOffset(parentSegment)) staggerElement = parentSegment;
499
526
  }
500
527
  }
501
- const effectiveDelay = calculateEffectiveDelay(timing.delay, staggerElement);
502
- mapAndSetAnimationTime(animation, timeSource, timing, effectiveDelay);
528
+ const resolvedStagger = staggerElement ?? timeSource ?? fallbackElement;
529
+ const effectiveDelay = calculateEffectiveDelay(timing.delay, resolvedStagger);
530
+ const data = {
531
+ timing,
532
+ timeSource,
533
+ staggerElement,
534
+ effectiveDelay
535
+ };
536
+ animationCache.set(animation, data);
537
+ validateAnimationFillMode(animation, timing);
538
+ return data;
539
+ };
540
+ /**
541
+ * Synchronizes a single animation with the timeline using the element as the time source.
542
+ *
543
+ * Timing, time-source, and stagger lookups are cached per animation — they are derived
544
+ * from immutable properties (keyframe timing, DOM parent chain) and never change during
545
+ * the lifetime of an animation.
546
+ */
547
+ const synchronizeAnimation = (animation, element) => {
548
+ const effect = animation.effect;
549
+ if (!validateAnimationEffect(effect)) return;
550
+ let cached = animationCache.get(animation);
551
+ if (!cached) cached = buildAnimationCache(animation, effect, element);
552
+ const { timing } = cached;
553
+ if (timing.duration <= 0) {
554
+ animation.currentTime = 0;
555
+ return;
556
+ }
557
+ mapAndSetAnimationTime(animation, cached.timeSource ?? element, timing, cached.effectiveDelay);
503
558
  };
504
559
  /**
505
560
  * Coordinates animations for a single element and its subtree, using the element as the time source.
@@ -530,6 +585,7 @@ const applyVisualState = (element, state) => {
530
585
  element.style.setProperty(PROGRESS_PROPERTY, `${state.progress}`);
531
586
  if (!state.isVisible) {
532
587
  element.style.setProperty("display", "none");
588
+ cancelTrackedAnimations(element);
533
589
  return;
534
590
  }
535
591
  element.style.removeProperty("display");
@@ -609,11 +665,14 @@ const evaluateElementState = (element) => {
609
665
  * 4. Apply visual state (update CSS and display based on phase and policies)
610
666
  */
611
667
  const updateAnimations = (element) => {
612
- const allAnimations = element.getAnimations({ subtree: true });
613
668
  const rootContext = evaluateElementState(element);
614
669
  const timelineTimeMs = (element.rootTimegroup ?? element).currentTimeMs;
615
670
  const { elements: collectedElements, pruned } = deepGetTemporalElements(element, timelineTimeMs);
616
- for (const prunedElement of pruned) prunedElement.style.setProperty("display", "none");
671
+ for (const prunedElement of pruned) {
672
+ prunedElement.style.setProperty("display", "none");
673
+ cancelTrackedAnimations(prunedElement);
674
+ }
675
+ const allAnimations = element.getAnimations({ subtree: true });
617
676
  const childContexts = [];
618
677
  for (const temporalElement of collectedElements) if (!pruned.has(temporalElement)) childContexts.push(evaluateElementState(temporalElement));
619
678
  const visibleChildContexts = [];
@@ -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 = (animation: Animation, currentAnimations: Animation[]): boolean => {\n // Check if animation has been cancelled\n if (animation.playState === \"idle\" && !currentAnimations.includes(animation)) {\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 = 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 = 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 = 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 = \"before-start\" | \"active\" | \"at-end-boundary\" | \"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 = 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 = (phase: ElementPhase, element: AnimatableElement): 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 = (_phase: ElementPhase, _element: AnimatableElement): 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 = (element: AnimatableElement): 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 = (element: AnimatableElement): 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 = (element: AnimatableElement): 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 = (delay: number, element: AnimatableElement): 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 (segment.staggerOffsetMs !== undefined && segment.staggerOffsetMs !== null) {\n return delay + segment.staggerOffsetMs;\n }\n\n // Fallback to CSS variable if property not available\n let cssValue = (element as HTMLElement).style.getPropertyValue(\"--ef-stagger-offset\").trim();\n\n if (!cssValue) {\n cssValue = window.getComputedStyle(element).getPropertyValue(\"--ef-stagger-offset\").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 = (duration: number, iterations: number): 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 = (direction: string, currentIteration: number): 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(elementTime, effectiveDelay, duration, direction, maxSafeTime);\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 = (timing: AnimationTiming, maxSafeTime: number): 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 effect !== null && effect instanceof KeyframeEffect && effect.target !== null;\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 = (keyframes: Keyframe[]): \"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 = firstFrame && \"opacity\" in firstFrame ? Number(firstFrame.opacity) : null;\n const lastOpacity = 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 || \"translate\" in frame || \"scale\" in frame || \"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 = (animation: Animation, timing: AnimationTiming): void => {\n // Only validate in development mode\n if (typeof process !== \"undefined\" && process.env?.NODE_ENV === \"production\") {\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 ((fadePattern === \"fade-in\" || hasTransform) && fill !== \"backwards\" && fill !== \"both\") {\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(timing, maxSafeTime);\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(elementTime, timing, effectiveDelay);\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\") && 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 = (animation: Animation, element: AnimatableElement): 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 (parentSegment && supportsStaggerOffset(parentSegment as AnimatableElement)) {\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 } = discoverAndTrackAnimations(\n element,\n providedAnimations,\n );\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 = (element: AnimatableElement, state: TemporalState): 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 = (element: AnimatableElement): 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(element, timelineTimeMs);\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>(visibleChildContexts.map((c) => c.element));\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 = 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(rootContext.element, rootContext.state.phase, allAnimations);\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 | null","staggerElement: AnimatableElement | null","data: CachedAnimationData","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 * Tracks animations that have already been taken under manual control.\n * Once an animation is here, its playState is known to be \"paused\" or \"idle\" —\n * both accept currentTime writes without preconditions and without causing reflow.\n * This lets prepareAnimation skip cancel()/pause() on every subsequent frame.\n */\nconst preparedAnimations = new WeakSet<Animation>();\n\n/**\n * Per-animation data derived once from the animation's immutable properties:\n * timing (duration/delay/iterations/direction), time source (nearest ef-timegroup),\n * stagger element (nearest ef-text-segment), and effective delay.\n *\n * All of these are fixed for the lifetime of an animation — the target's position\n * in the DOM and the animation's keyframe timing never change during scrubbing.\n * Caching avoids effect.getTiming() and target.closest() on every frame.\n */\ninterface CachedAnimationData {\n timing: AnimationTiming;\n timeSource: AnimatableElement | null; // null means \"use the root element passed in\"\n staggerElement: AnimatableElement | null; // null means \"use timeSource\"\n effectiveDelay: number;\n}\nconst animationCache = new WeakMap<Animation, CachedAnimationData>();\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 = (animation: Animation, currentAnimations: Animation[]): boolean => {\n // Check if animation is no longer in the live animation list.\n // Avoid reading animation.playState — it forces style recalculation in Chromium.\n if (!currentAnimations.includes(animation)) {\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 = 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 = 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 = 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 * Returns the number of animations tracked for an element.\n * Exported for testing only.\n */\nexport const getTrackedAnimationCount = (element: Element): number => {\n return animationTracker.get(element)?.size ?? 0;\n};\n\n/**\n * Cancels all tracked animations for an element and removes them from tracking.\n * Called when an element is hidden so paused WAAPI animations leave getAnimations(),\n * preventing unbounded growth of getAnimations({subtree:true}) during scrubbing.\n */\nconst cancelTrackedAnimations = (element: Element): void => {\n // Cancel animations from the tracker. cancel() is safe on any state including idle.\n const tracked = animationTracker.get(element);\n if (tracked) {\n for (const animation of tracked) {\n animation.cancel();\n preparedAnimations.delete(animation);\n animationCache.delete(animation);\n }\n tracked.clear();\n }\n // Also cancel any live animations on the element's subtree that may not be tracked yet.\n // getAnimations() only returns non-idle animations, so no guard needed.\n const subtreeAnims = (element as HTMLElement).getAnimations?.({ subtree: true });\n if (subtreeAnims) {\n for (const animation of subtreeAnims) {\n animation.cancel();\n preparedAnimations.delete(animation);\n animationCache.delete(animation);\n }\n }\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 = \"before-start\" | \"active\" | \"at-end-boundary\" | \"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 = 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 = (phase: ElementPhase, element: AnimatableElement): 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 = (_phase: ElementPhase, _element: AnimatableElement): 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 = (element: AnimatableElement): 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 = (element: AnimatableElement): 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 = (element: AnimatableElement): 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 = (delay: number, element: AnimatableElement): 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 (segment.staggerOffsetMs !== undefined && segment.staggerOffsetMs !== null) {\n return delay + segment.staggerOffsetMs;\n }\n\n // Fallback to CSS variable if property not available\n let cssValue = (element as HTMLElement).style.getPropertyValue(\"--ef-stagger-offset\").trim();\n\n if (!cssValue) {\n cssValue = window.getComputedStyle(element).getPropertyValue(\"--ef-stagger-offset\").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 = (duration: number, iterations: number): 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 = (direction: string, currentIteration: number): 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(elementTime, effectiveDelay, duration, direction, maxSafeTime);\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 = (timing: AnimationTiming, maxSafeTime: number): 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 effect !== null && effect instanceof KeyframeEffect && effect.target !== null;\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 = (keyframes: Keyframe[]): \"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 = firstFrame && \"opacity\" in firstFrame ? Number(firstFrame.opacity) : null;\n const lastOpacity = 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 || \"translate\" in frame || \"scale\" in frame || \"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 = (animation: Animation, timing: AnimationTiming): void => {\n // Only validate in development mode\n if (typeof process !== \"undefined\" && process.env?.NODE_ENV === \"production\") {\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 ((fadePattern === \"fade-in\" || hasTransform) && fill !== \"backwards\" && fill !== \"both\") {\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 on first encounter.\n *\n * Reading animation.playState forces style recalculation in Chromium (layout thrash).\n * Instead we track prepared animations in a WeakSet. On first encounter we optimistically\n * cancel the animation — cancel() is safe on any state (paused/running/finished/idle)\n * and leaves the animation in \"idle\", from which currentTime writes work freely.\n * On subsequent frames the animation is already under our control so we skip this entirely.\n */\nconst prepareAnimation = (animation: Animation): void => {\n if (preparedAnimations.has(animation)) {\n return;\n }\n animation.cancel();\n preparedAnimations.add(animation);\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 // 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(timing, maxSafeTime);\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(elementTime, timing, effectiveDelay);\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\") && 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 * Builds and caches per-animation data derived from immutable properties.\n * Called once per animation on first synchronization; subsequent frames use the cache.\n */\nconst buildAnimationCache = (\n animation: Animation,\n effect: KeyframeEffect & { target: Element },\n fallbackElement: AnimatableElement,\n): CachedAnimationData => {\n const timing = extractAnimationTiming(effect);\n const target = effect.target;\n\n // Resolve time source: nearest ef-timegroup ancestor (or fallback to root)\n let timeSource: AnimatableElement | null = null;\n if (target instanceof HTMLElement) {\n const nearestTimegroup = target.closest(\"ef-timegroup\");\n if (nearestTimegroup && isEFTemporal(nearestTimegroup)) {\n timeSource = nearestTimegroup as AnimatableElement;\n }\n }\n\n // Resolve stagger element: nearest ef-text-segment ancestor (or fallback to timeSource)\n let staggerElement: AnimatableElement | null = null;\n if (target instanceof HTMLElement) {\n const targetAsAnimatable = target as AnimatableElement;\n if (supportsStaggerOffset(targetAsAnimatable)) {\n staggerElement = targetAsAnimatable;\n } else {\n const parentSegment = target.closest(\"ef-text-segment\");\n if (parentSegment && supportsStaggerOffset(parentSegment as AnimatableElement)) {\n staggerElement = parentSegment as AnimatableElement;\n }\n }\n }\n\n const resolvedStagger = staggerElement ?? timeSource ?? fallbackElement;\n const effectiveDelay = calculateEffectiveDelay(timing.delay, resolvedStagger);\n\n const data: CachedAnimationData = { timing, timeSource, staggerElement, effectiveDelay };\n animationCache.set(animation, data);\n\n // Validate fill-mode once at cache-build time (first encounter per animation).\n // Moved here from synchronizeAnimation to avoid re-entering on every subsequent frame.\n validateAnimationFillMode(animation, timing);\n\n return data;\n};\n\n/**\n * Synchronizes a single animation with the timeline using the element as the time source.\n *\n * Timing, time-source, and stagger lookups are cached per animation — they are derived\n * from immutable properties (keyframe timing, DOM parent chain) and never change during\n * the lifetime of an animation.\n */\nconst synchronizeAnimation = (animation: Animation, element: AnimatableElement): void => {\n const effect = animation.effect;\n if (!validateAnimationEffect(effect)) {\n return;\n }\n\n // Use cached data when available; build and cache on first encounter\n let cached = animationCache.get(animation);\n if (!cached) {\n cached = buildAnimationCache(animation, effect, element);\n }\n\n const { timing } = cached;\n\n if (timing.duration <= 0) {\n animation.currentTime = 0;\n return;\n }\n\n const timeSource = cached.timeSource ?? element;\n mapAndSetAnimationTime(animation, timeSource, timing, cached.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 } = discoverAndTrackAnimations(\n element,\n providedAnimations,\n );\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 = (element: AnimatableElement, state: TemporalState): 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 cancelTrackedAnimations(element);\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 = (element: AnimatableElement): 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 rootContext = evaluateElementState(element);\n const timelineTimeMs = (element.rootTimegroup ?? element).currentTimeMs;\n const { elements: collectedElements, pruned } = deepGetTemporalElements(element, timelineTimeMs);\n\n // For pruned elements (invisible containers whose subtrees were skipped),\n // cancel their animations and hide them before fetching allAnimations.\n // This ensures the subsequent getAnimations() call reflects only active animations.\n for (const prunedElement of pruned) {\n prunedElement.style.setProperty(\"display\", \"none\");\n cancelTrackedAnimations(prunedElement);\n }\n\n // Fetch allAnimations after pruned elements are cancelled so they don't appear in the list.\n const allAnimations = element.getAnimations({ subtree: true });\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>(visibleChildContexts.map((c) => c.element));\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 = 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(rootContext.element, rootContext.state.phase, allAnimations);\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,5 +1,5 @@
1
1
  //#region src/gui/TWMixin.css?inline
2
- var TWMixin_default = "/* biome-ignore lint/suspicious/noUnknownAtRules: @tailwind is a valid Tailwind CSS directive */\n*, ::before, ::after {\n --tw-border-spacing-x: 0;\n --tw-border-spacing-y: 0;\n --tw-translate-x: 0;\n --tw-translate-y: 0;\n --tw-rotate: 0;\n --tw-skew-x: 0;\n --tw-skew-y: 0;\n --tw-scale-x: 1;\n --tw-scale-y: 1;\n --tw-pan-x: ;\n --tw-pan-y: ;\n --tw-pinch-zoom: ;\n --tw-scroll-snap-strictness: proximity;\n --tw-gradient-from-position: ;\n --tw-gradient-via-position: ;\n --tw-gradient-to-position: ;\n --tw-ordinal: ;\n --tw-slashed-zero: ;\n --tw-numeric-figure: ;\n --tw-numeric-spacing: ;\n --tw-numeric-fraction: ;\n --tw-ring-inset: ;\n --tw-ring-offset-width: 0px;\n --tw-ring-offset-color: #fff;\n --tw-ring-color: rgb(59 130 246 / 0.5);\n --tw-ring-offset-shadow: 0 0 #0000;\n --tw-ring-shadow: 0 0 #0000;\n --tw-shadow: 0 0 #0000;\n --tw-shadow-colored: 0 0 #0000;\n --tw-blur: ;\n --tw-brightness: ;\n --tw-contrast: ;\n --tw-grayscale: ;\n --tw-hue-rotate: ;\n --tw-invert: ;\n --tw-saturate: ;\n --tw-sepia: ;\n --tw-drop-shadow: ;\n --tw-backdrop-blur: ;\n --tw-backdrop-brightness: ;\n --tw-backdrop-contrast: ;\n --tw-backdrop-grayscale: ;\n --tw-backdrop-hue-rotate: ;\n --tw-backdrop-invert: ;\n --tw-backdrop-opacity: ;\n --tw-backdrop-saturate: ;\n --tw-backdrop-sepia: ;\n --tw-contain-size: ;\n --tw-contain-layout: ;\n --tw-contain-paint: ;\n --tw-contain-style: ;\n}\n::backdrop {\n --tw-border-spacing-x: 0;\n --tw-border-spacing-y: 0;\n --tw-translate-x: 0;\n --tw-translate-y: 0;\n --tw-rotate: 0;\n --tw-skew-x: 0;\n --tw-skew-y: 0;\n --tw-scale-x: 1;\n --tw-scale-y: 1;\n --tw-pan-x: ;\n --tw-pan-y: ;\n --tw-pinch-zoom: ;\n --tw-scroll-snap-strictness: proximity;\n --tw-gradient-from-position: ;\n --tw-gradient-via-position: ;\n --tw-gradient-to-position: ;\n --tw-ordinal: ;\n --tw-slashed-zero: ;\n --tw-numeric-figure: ;\n --tw-numeric-spacing: ;\n --tw-numeric-fraction: ;\n --tw-ring-inset: ;\n --tw-ring-offset-width: 0px;\n --tw-ring-offset-color: #fff;\n --tw-ring-color: rgb(59 130 246 / 0.5);\n --tw-ring-offset-shadow: 0 0 #0000;\n --tw-ring-shadow: 0 0 #0000;\n --tw-shadow: 0 0 #0000;\n --tw-shadow-colored: 0 0 #0000;\n --tw-blur: ;\n --tw-brightness: ;\n --tw-contrast: ;\n --tw-grayscale: ;\n --tw-hue-rotate: ;\n --tw-invert: ;\n --tw-saturate: ;\n --tw-sepia: ;\n --tw-drop-shadow: ;\n --tw-backdrop-blur: ;\n --tw-backdrop-brightness: ;\n --tw-backdrop-contrast: ;\n --tw-backdrop-grayscale: ;\n --tw-backdrop-hue-rotate: ;\n --tw-backdrop-invert: ;\n --tw-backdrop-opacity: ;\n --tw-backdrop-saturate: ;\n --tw-backdrop-sepia: ;\n --tw-contain-size: ;\n --tw-contain-layout: ;\n --tw-contain-paint: ;\n --tw-contain-style: ;\n}\n/* ! tailwindcss v3.4.18 | MIT License | https://tailwindcss.com */\n/*\n1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)\n2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)\n*/\n*,\n::before,\n::after {\n box-sizing: border-box; /* 1 */\n border-width: 0; /* 2 */\n border-style: solid; /* 2 */\n border-color: #e5e7eb; /* 2 */\n}\n::before,\n::after {\n --tw-content: '';\n}\n/*\n1. Use a consistent sensible line-height in all browsers.\n2. Prevent adjustments of font size after orientation changes in iOS.\n3. Use a more readable tab size.\n4. Use the user's configured `sans` font-family by default.\n5. Use the user's configured `sans` font-feature-settings by default.\n6. Use the user's configured `sans` font-variation-settings by default.\n7. Disable tap highlights on iOS\n*/\nhtml,\n:host {\n line-height: 1.5; /* 1 */\n -webkit-text-size-adjust: 100%; /* 2 */\n -moz-tab-size: 4; /* 3 */\n -o-tab-size: 4;\n tab-size: 4; /* 3 */\n font-family: ui-sans-serif, system-ui, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\"; /* 4 */\n font-feature-settings: normal; /* 5 */\n font-variation-settings: normal; /* 6 */\n -webkit-tap-highlight-color: transparent; /* 7 */\n}\n/*\n1. Remove the margin in all browsers.\n2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.\n*/\nbody {\n margin: 0; /* 1 */\n line-height: inherit; /* 2 */\n}\n/*\n1. Add the correct height in Firefox.\n2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)\n3. Ensure horizontal rules are visible by default.\n*/\nhr {\n height: 0; /* 1 */\n color: inherit; /* 2 */\n border-top-width: 1px; /* 3 */\n}\n/*\nAdd the correct text decoration in Chrome, Edge, and Safari.\n*/\nabbr:where([title]) {\n -webkit-text-decoration: underline dotted;\n text-decoration: underline dotted;\n}\n/*\nRemove the default font size and weight for headings.\n*/\nh1,\nh2,\nh3,\nh4,\nh5,\nh6 {\n font-size: inherit;\n font-weight: inherit;\n}\n/*\nReset links to optimize for opt-in styling instead of opt-out.\n*/\na {\n color: inherit;\n text-decoration: inherit;\n}\n/*\nAdd the correct font weight in Edge and Safari.\n*/\nb,\nstrong {\n font-weight: bolder;\n}\n/*\n1. Use the user's configured `mono` font-family by default.\n2. Use the user's configured `mono` font-feature-settings by default.\n3. Use the user's configured `mono` font-variation-settings by default.\n4. Correct the odd `em` font sizing in all browsers.\n*/\ncode,\nkbd,\nsamp,\npre {\n font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace; /* 1 */\n font-feature-settings: normal; /* 2 */\n font-variation-settings: normal; /* 3 */\n font-size: 1em; /* 4 */\n}\n/*\nAdd the correct font size in all browsers.\n*/\nsmall {\n font-size: 80%;\n}\n/*\nPrevent `sub` and `sup` elements from affecting the line height in all browsers.\n*/\nsub,\nsup {\n font-size: 75%;\n line-height: 0;\n position: relative;\n vertical-align: baseline;\n}\nsub {\n bottom: -0.25em;\n}\nsup {\n top: -0.5em;\n}\n/*\n1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)\n2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)\n3. Remove gaps between table borders by default.\n*/\ntable {\n text-indent: 0; /* 1 */\n border-color: inherit; /* 2 */\n border-collapse: collapse; /* 3 */\n}\n/*\n1. Change the font styles in all browsers.\n2. Remove the margin in Firefox and Safari.\n3. Remove default padding in all browsers.\n*/\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n font-family: inherit; /* 1 */\n font-feature-settings: inherit; /* 1 */\n font-variation-settings: inherit; /* 1 */\n font-size: 100%; /* 1 */\n font-weight: inherit; /* 1 */\n line-height: inherit; /* 1 */\n letter-spacing: inherit; /* 1 */\n color: inherit; /* 1 */\n margin: 0; /* 2 */\n padding: 0; /* 3 */\n}\n/*\nRemove the inheritance of text transform in Edge and Firefox.\n*/\nbutton,\nselect {\n text-transform: none;\n}\n/*\n1. Correct the inability to style clickable types in iOS and Safari.\n2. Remove default button styles.\n*/\nbutton,\ninput:where([type='button']),\ninput:where([type='reset']),\ninput:where([type='submit']) {\n -webkit-appearance: button; /* 1 */\n background-color: transparent; /* 2 */\n background-image: none; /* 2 */\n}\n/*\nUse the modern Firefox focus style for all focusable elements.\n*/\n:-moz-focusring {\n outline: auto;\n}\n/*\nRemove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)\n*/\n:-moz-ui-invalid {\n box-shadow: none;\n}\n/*\nAdd the correct vertical alignment in Chrome and Firefox.\n*/\nprogress {\n vertical-align: baseline;\n}\n/*\nCorrect the cursor style of increment and decrement buttons in Safari.\n*/\n::-webkit-inner-spin-button,\n::-webkit-outer-spin-button {\n height: auto;\n}\n/*\n1. Correct the odd appearance in Chrome and Safari.\n2. Correct the outline style in Safari.\n*/\n[type='search'] {\n -webkit-appearance: textfield; /* 1 */\n outline-offset: -2px; /* 2 */\n}\n/*\nRemove the inner padding in Chrome and Safari on macOS.\n*/\n::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n/*\n1. Correct the inability to style clickable types in iOS and Safari.\n2. Change font properties to `inherit` in Safari.\n*/\n::-webkit-file-upload-button {\n -webkit-appearance: button; /* 1 */\n font: inherit; /* 2 */\n}\n/*\nAdd the correct display in Chrome and Safari.\n*/\nsummary {\n display: list-item;\n}\n/*\nRemoves the default spacing and border for appropriate elements.\n*/\nblockquote,\ndl,\ndd,\nh1,\nh2,\nh3,\nh4,\nh5,\nh6,\nhr,\nfigure,\np,\npre {\n margin: 0;\n}\nfieldset {\n margin: 0;\n padding: 0;\n}\nlegend {\n padding: 0;\n}\nol,\nul,\nmenu {\n list-style: none;\n margin: 0;\n padding: 0;\n}\n/*\nReset default styling for dialogs.\n*/\ndialog {\n padding: 0;\n}\n/*\nPrevent resizing textareas horizontally by default.\n*/\ntextarea {\n resize: vertical;\n}\n/*\n1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)\n2. Set the default placeholder color to the user's configured gray 400 color.\n*/\ninput::-moz-placeholder, textarea::-moz-placeholder {\n opacity: 1; /* 1 */\n color: #9ca3af; /* 2 */\n}\ninput::placeholder,\ntextarea::placeholder {\n opacity: 1; /* 1 */\n color: #9ca3af; /* 2 */\n}\n/*\nSet the default cursor for buttons.\n*/\nbutton,\n[role=\"button\"] {\n cursor: pointer;\n}\n/*\nMake sure disabled buttons don't get the pointer cursor.\n*/\n:disabled {\n cursor: default;\n}\n/*\n1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)\n2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)\n This can trigger a poorly considered lint error in some tools but is included by design.\n*/\nimg,\nsvg,\nvideo,\ncanvas,\naudio,\niframe,\nembed,\nobject {\n display: block; /* 1 */\n vertical-align: middle; /* 2 */\n}\n/*\nConstrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)\n*/\nimg,\nvideo {\n max-width: 100%;\n height: auto;\n}\n/* Make elements with the HTML hidden attribute stay hidden by default */\n[hidden]:where(:not([hidden=\"until-found\"])) {\n display: none;\n}\n/* biome-ignore lint/suspicious/noUnknownAtRules: @tailwind is a valid Tailwind CSS directive */\n.\\!container {\n width: 100% !important;\n}\n.container {\n width: 100%;\n}\n@media (min-width: 640px) {\n .\\!container {\n max-width: 640px !important;\n }\n .container {\n max-width: 640px;\n }\n}\n@media (min-width: 768px) {\n .\\!container {\n max-width: 768px !important;\n }\n .container {\n max-width: 768px;\n }\n}\n@media (min-width: 1024px) {\n .\\!container {\n max-width: 1024px !important;\n }\n .container {\n max-width: 1024px;\n }\n}\n@media (min-width: 1280px) {\n .\\!container {\n max-width: 1280px !important;\n }\n .container {\n max-width: 1280px;\n }\n}\n@media (min-width: 1536px) {\n .\\!container {\n max-width: 1536px !important;\n }\n .container {\n max-width: 1536px;\n }\n}\n/* biome-ignore lint/suspicious/noUnknownAtRules: @tailwind is a valid Tailwind CSS directive */\n.visible {\n visibility: visible;\n}\n.invisible {\n visibility: hidden;\n}\n.collapse {\n visibility: collapse;\n}\n.static {\n position: static;\n}\n.fixed {\n position: fixed;\n}\n.absolute {\n position: absolute;\n}\n.relative {\n position: relative;\n}\n.sticky {\n position: sticky;\n}\n.inset-0 {\n inset: 0px;\n}\n.left-0 {\n left: 0px;\n}\n.right-0 {\n right: 0px;\n}\n.top-0 {\n top: 0px;\n}\n.top-8 {\n top: 2rem;\n}\n.isolate {\n isolation: isolate;\n}\n.z-\\[5\\] {\n z-index: 5;\n}\n.mb-0 {\n margin-bottom: 0px;\n}\n.mb-\\[1px\\] {\n margin-bottom: 1px;\n}\n.block {\n display: block;\n}\n.inline-block {\n display: inline-block;\n}\n.inline {\n display: inline;\n}\n.flex {\n display: flex;\n}\n.inline-flex {\n display: inline-flex;\n}\n.table {\n display: table;\n}\n.grid {\n display: grid;\n}\n.inline-grid {\n display: inline-grid;\n}\n.contents {\n display: contents;\n}\n.hidden {\n display: none;\n}\n.size-full {\n width: 100%;\n height: 100%;\n}\n.h-\\[1\\.1rem\\] {\n height: 1.1rem;\n}\n.h-\\[1080px\\] {\n height: 1080px;\n}\n.h-\\[500px\\] {\n height: 500px;\n}\n.h-\\[5px\\] {\n height: 5px;\n}\n.h-\\[calc\\(50vh-4rem\\)\\] {\n height: calc(50vh - 4rem);\n}\n.h-full {\n height: 100%;\n}\n.w-1 {\n width: 0.25rem;\n}\n.w-\\[1000px\\] {\n width: 1000px;\n}\n.w-\\[1920px\\] {\n width: 1920px;\n}\n.w-\\[420px\\] {\n width: 420px;\n}\n.w-full {\n width: 100%;\n}\n.flex-1 {\n flex: 1 1 0%;\n}\n.flex-shrink {\n flex-shrink: 1;\n}\n.shrink {\n flex-shrink: 1;\n}\n.\\!transform {\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)) !important;\n}\n.transform {\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.resize {\n resize: both;\n}\n.flex-wrap {\n flex-wrap: wrap;\n}\n.items-center {\n align-items: center;\n}\n.overflow-hidden {\n overflow: hidden;\n}\n.overflow-visible {\n overflow: visible;\n}\n.whitespace-nowrap {\n white-space: nowrap;\n}\n.text-nowrap {\n text-wrap: nowrap;\n}\n.rounded {\n border-radius: 0.25rem;\n}\n.border {\n border-width: 1px;\n}\n.bg-slate-500 {\n --tw-bg-opacity: 1;\n background-color: rgb(100 116 139 / var(--tw-bg-opacity, 1));\n}\n.object-cover {\n -o-object-fit: cover;\n object-fit: cover;\n}\n.px-0\\.5 {\n padding-left: 0.125rem;\n padding-right: 0.125rem;\n}\n.text-center {\n text-align: center;\n}\n.text-3xl {\n font-size: 1.875rem;\n line-height: 2.25rem;\n}\n.text-\\[8px\\] {\n font-size: 8px;\n}\n.text-sm {\n font-size: 0.875rem;\n line-height: 1.25rem;\n}\n.text-xs {\n font-size: 0.75rem;\n line-height: 1rem;\n}\n.font-bold {\n font-weight: 700;\n}\n.uppercase {\n text-transform: uppercase;\n}\n.capitalize {\n text-transform: capitalize;\n}\n.italic {\n font-style: italic;\n}\n.text-white {\n --tw-text-opacity: 1;\n color: rgb(255 255 255 / var(--tw-text-opacity, 1));\n}\n.opacity-50 {\n opacity: 0.5;\n}\n.shadow {\n --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);\n --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);\n box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);\n}\n.outline {\n outline-style: solid;\n}\n.blur {\n --tw-blur: blur(8px);\n filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);\n}\n.grayscale {\n --tw-grayscale: grayscale(100%);\n filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);\n}\n.filter {\n filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);\n}\n.backdrop-filter {\n backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);\n}\n.transition {\n transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;\n transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n transition-duration: 150ms;\n}\n.ease-in {\n transition-timing-function: cubic-bezier(0.4, 0, 1, 1);\n}\n.ease-in-out {\n transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n}\n.ease-out {\n transition-timing-function: cubic-bezier(0, 0, 0.2, 1);\n}\n";
2
+ var TWMixin_default = "/* biome-ignore lint/suspicious/noUnknownAtRules: @tailwind is a valid Tailwind CSS directive */\n*, ::before, ::after {\n --tw-border-spacing-x: 0;\n --tw-border-spacing-y: 0;\n --tw-translate-x: 0;\n --tw-translate-y: 0;\n --tw-rotate: 0;\n --tw-skew-x: 0;\n --tw-skew-y: 0;\n --tw-scale-x: 1;\n --tw-scale-y: 1;\n --tw-pan-x: ;\n --tw-pan-y: ;\n --tw-pinch-zoom: ;\n --tw-scroll-snap-strictness: proximity;\n --tw-gradient-from-position: ;\n --tw-gradient-via-position: ;\n --tw-gradient-to-position: ;\n --tw-ordinal: ;\n --tw-slashed-zero: ;\n --tw-numeric-figure: ;\n --tw-numeric-spacing: ;\n --tw-numeric-fraction: ;\n --tw-ring-inset: ;\n --tw-ring-offset-width: 0px;\n --tw-ring-offset-color: #fff;\n --tw-ring-color: rgb(59 130 246 / 0.5);\n --tw-ring-offset-shadow: 0 0 #0000;\n --tw-ring-shadow: 0 0 #0000;\n --tw-shadow: 0 0 #0000;\n --tw-shadow-colored: 0 0 #0000;\n --tw-blur: ;\n --tw-brightness: ;\n --tw-contrast: ;\n --tw-grayscale: ;\n --tw-hue-rotate: ;\n --tw-invert: ;\n --tw-saturate: ;\n --tw-sepia: ;\n --tw-drop-shadow: ;\n --tw-backdrop-blur: ;\n --tw-backdrop-brightness: ;\n --tw-backdrop-contrast: ;\n --tw-backdrop-grayscale: ;\n --tw-backdrop-hue-rotate: ;\n --tw-backdrop-invert: ;\n --tw-backdrop-opacity: ;\n --tw-backdrop-saturate: ;\n --tw-backdrop-sepia: ;\n --tw-contain-size: ;\n --tw-contain-layout: ;\n --tw-contain-paint: ;\n --tw-contain-style: ;\n}\n::backdrop {\n --tw-border-spacing-x: 0;\n --tw-border-spacing-y: 0;\n --tw-translate-x: 0;\n --tw-translate-y: 0;\n --tw-rotate: 0;\n --tw-skew-x: 0;\n --tw-skew-y: 0;\n --tw-scale-x: 1;\n --tw-scale-y: 1;\n --tw-pan-x: ;\n --tw-pan-y: ;\n --tw-pinch-zoom: ;\n --tw-scroll-snap-strictness: proximity;\n --tw-gradient-from-position: ;\n --tw-gradient-via-position: ;\n --tw-gradient-to-position: ;\n --tw-ordinal: ;\n --tw-slashed-zero: ;\n --tw-numeric-figure: ;\n --tw-numeric-spacing: ;\n --tw-numeric-fraction: ;\n --tw-ring-inset: ;\n --tw-ring-offset-width: 0px;\n --tw-ring-offset-color: #fff;\n --tw-ring-color: rgb(59 130 246 / 0.5);\n --tw-ring-offset-shadow: 0 0 #0000;\n --tw-ring-shadow: 0 0 #0000;\n --tw-shadow: 0 0 #0000;\n --tw-shadow-colored: 0 0 #0000;\n --tw-blur: ;\n --tw-brightness: ;\n --tw-contrast: ;\n --tw-grayscale: ;\n --tw-hue-rotate: ;\n --tw-invert: ;\n --tw-saturate: ;\n --tw-sepia: ;\n --tw-drop-shadow: ;\n --tw-backdrop-blur: ;\n --tw-backdrop-brightness: ;\n --tw-backdrop-contrast: ;\n --tw-backdrop-grayscale: ;\n --tw-backdrop-hue-rotate: ;\n --tw-backdrop-invert: ;\n --tw-backdrop-opacity: ;\n --tw-backdrop-saturate: ;\n --tw-backdrop-sepia: ;\n --tw-contain-size: ;\n --tw-contain-layout: ;\n --tw-contain-paint: ;\n --tw-contain-style: ;\n}\n/* ! tailwindcss v3.4.18 | MIT License | https://tailwindcss.com */\n/*\n1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)\n2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)\n*/\n*,\n::before,\n::after {\n box-sizing: border-box; /* 1 */\n border-width: 0; /* 2 */\n border-style: solid; /* 2 */\n border-color: #e5e7eb; /* 2 */\n}\n::before,\n::after {\n --tw-content: '';\n}\n/*\n1. Use a consistent sensible line-height in all browsers.\n2. Prevent adjustments of font size after orientation changes in iOS.\n3. Use a more readable tab size.\n4. Use the user's configured `sans` font-family by default.\n5. Use the user's configured `sans` font-feature-settings by default.\n6. Use the user's configured `sans` font-variation-settings by default.\n7. Disable tap highlights on iOS\n*/\nhtml,\n:host {\n line-height: 1.5; /* 1 */\n -webkit-text-size-adjust: 100%; /* 2 */\n -moz-tab-size: 4; /* 3 */\n -o-tab-size: 4;\n tab-size: 4; /* 3 */\n font-family: ui-sans-serif, system-ui, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\"; /* 4 */\n font-feature-settings: normal; /* 5 */\n font-variation-settings: normal; /* 6 */\n -webkit-tap-highlight-color: transparent; /* 7 */\n}\n/*\n1. Remove the margin in all browsers.\n2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.\n*/\nbody {\n margin: 0; /* 1 */\n line-height: inherit; /* 2 */\n}\n/*\n1. Add the correct height in Firefox.\n2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)\n3. Ensure horizontal rules are visible by default.\n*/\nhr {\n height: 0; /* 1 */\n color: inherit; /* 2 */\n border-top-width: 1px; /* 3 */\n}\n/*\nAdd the correct text decoration in Chrome, Edge, and Safari.\n*/\nabbr:where([title]) {\n -webkit-text-decoration: underline dotted;\n text-decoration: underline dotted;\n}\n/*\nRemove the default font size and weight for headings.\n*/\nh1,\nh2,\nh3,\nh4,\nh5,\nh6 {\n font-size: inherit;\n font-weight: inherit;\n}\n/*\nReset links to optimize for opt-in styling instead of opt-out.\n*/\na {\n color: inherit;\n text-decoration: inherit;\n}\n/*\nAdd the correct font weight in Edge and Safari.\n*/\nb,\nstrong {\n font-weight: bolder;\n}\n/*\n1. Use the user's configured `mono` font-family by default.\n2. Use the user's configured `mono` font-feature-settings by default.\n3. Use the user's configured `mono` font-variation-settings by default.\n4. Correct the odd `em` font sizing in all browsers.\n*/\ncode,\nkbd,\nsamp,\npre {\n font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace; /* 1 */\n font-feature-settings: normal; /* 2 */\n font-variation-settings: normal; /* 3 */\n font-size: 1em; /* 4 */\n}\n/*\nAdd the correct font size in all browsers.\n*/\nsmall {\n font-size: 80%;\n}\n/*\nPrevent `sub` and `sup` elements from affecting the line height in all browsers.\n*/\nsub,\nsup {\n font-size: 75%;\n line-height: 0;\n position: relative;\n vertical-align: baseline;\n}\nsub {\n bottom: -0.25em;\n}\nsup {\n top: -0.5em;\n}\n/*\n1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)\n2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)\n3. Remove gaps between table borders by default.\n*/\ntable {\n text-indent: 0; /* 1 */\n border-color: inherit; /* 2 */\n border-collapse: collapse; /* 3 */\n}\n/*\n1. Change the font styles in all browsers.\n2. Remove the margin in Firefox and Safari.\n3. Remove default padding in all browsers.\n*/\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n font-family: inherit; /* 1 */\n font-feature-settings: inherit; /* 1 */\n font-variation-settings: inherit; /* 1 */\n font-size: 100%; /* 1 */\n font-weight: inherit; /* 1 */\n line-height: inherit; /* 1 */\n letter-spacing: inherit; /* 1 */\n color: inherit; /* 1 */\n margin: 0; /* 2 */\n padding: 0; /* 3 */\n}\n/*\nRemove the inheritance of text transform in Edge and Firefox.\n*/\nbutton,\nselect {\n text-transform: none;\n}\n/*\n1. Correct the inability to style clickable types in iOS and Safari.\n2. Remove default button styles.\n*/\nbutton,\ninput:where([type='button']),\ninput:where([type='reset']),\ninput:where([type='submit']) {\n -webkit-appearance: button; /* 1 */\n background-color: transparent; /* 2 */\n background-image: none; /* 2 */\n}\n/*\nUse the modern Firefox focus style for all focusable elements.\n*/\n:-moz-focusring {\n outline: auto;\n}\n/*\nRemove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)\n*/\n:-moz-ui-invalid {\n box-shadow: none;\n}\n/*\nAdd the correct vertical alignment in Chrome and Firefox.\n*/\nprogress {\n vertical-align: baseline;\n}\n/*\nCorrect the cursor style of increment and decrement buttons in Safari.\n*/\n::-webkit-inner-spin-button,\n::-webkit-outer-spin-button {\n height: auto;\n}\n/*\n1. Correct the odd appearance in Chrome and Safari.\n2. Correct the outline style in Safari.\n*/\n[type='search'] {\n -webkit-appearance: textfield; /* 1 */\n outline-offset: -2px; /* 2 */\n}\n/*\nRemove the inner padding in Chrome and Safari on macOS.\n*/\n::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n/*\n1. Correct the inability to style clickable types in iOS and Safari.\n2. Change font properties to `inherit` in Safari.\n*/\n::-webkit-file-upload-button {\n -webkit-appearance: button; /* 1 */\n font: inherit; /* 2 */\n}\n/*\nAdd the correct display in Chrome and Safari.\n*/\nsummary {\n display: list-item;\n}\n/*\nRemoves the default spacing and border for appropriate elements.\n*/\nblockquote,\ndl,\ndd,\nh1,\nh2,\nh3,\nh4,\nh5,\nh6,\nhr,\nfigure,\np,\npre {\n margin: 0;\n}\nfieldset {\n margin: 0;\n padding: 0;\n}\nlegend {\n padding: 0;\n}\nol,\nul,\nmenu {\n list-style: none;\n margin: 0;\n padding: 0;\n}\n/*\nReset default styling for dialogs.\n*/\ndialog {\n padding: 0;\n}\n/*\nPrevent resizing textareas horizontally by default.\n*/\ntextarea {\n resize: vertical;\n}\n/*\n1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)\n2. Set the default placeholder color to the user's configured gray 400 color.\n*/\ninput::-moz-placeholder, textarea::-moz-placeholder {\n opacity: 1; /* 1 */\n color: #9ca3af; /* 2 */\n}\ninput::placeholder,\ntextarea::placeholder {\n opacity: 1; /* 1 */\n color: #9ca3af; /* 2 */\n}\n/*\nSet the default cursor for buttons.\n*/\nbutton,\n[role=\"button\"] {\n cursor: pointer;\n}\n/*\nMake sure disabled buttons don't get the pointer cursor.\n*/\n:disabled {\n cursor: default;\n}\n/*\n1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)\n2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)\n This can trigger a poorly considered lint error in some tools but is included by design.\n*/\nimg,\nsvg,\nvideo,\ncanvas,\naudio,\niframe,\nembed,\nobject {\n display: block; /* 1 */\n vertical-align: middle; /* 2 */\n}\n/*\nConstrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)\n*/\nimg,\nvideo {\n max-width: 100%;\n height: auto;\n}\n/* Make elements with the HTML hidden attribute stay hidden by default */\n[hidden]:where(:not([hidden=\"until-found\"])) {\n display: none;\n}\n/* biome-ignore lint/suspicious/noUnknownAtRules: @tailwind is a valid Tailwind CSS directive */\n.\\!container {\n width: 100% !important;\n}\n.container {\n width: 100%;\n}\n@media (min-width: 640px) {\n .\\!container {\n max-width: 640px !important;\n }\n .container {\n max-width: 640px;\n }\n}\n@media (min-width: 768px) {\n .\\!container {\n max-width: 768px !important;\n }\n .container {\n max-width: 768px;\n }\n}\n@media (min-width: 1024px) {\n .\\!container {\n max-width: 1024px !important;\n }\n .container {\n max-width: 1024px;\n }\n}\n@media (min-width: 1280px) {\n .\\!container {\n max-width: 1280px !important;\n }\n .container {\n max-width: 1280px;\n }\n}\n@media (min-width: 1536px) {\n .\\!container {\n max-width: 1536px !important;\n }\n .container {\n max-width: 1536px;\n }\n}\n/* biome-ignore lint/suspicious/noUnknownAtRules: @tailwind is a valid Tailwind CSS directive */\n.visible {\n visibility: visible;\n}\n.invisible {\n visibility: hidden;\n}\n.collapse {\n visibility: collapse;\n}\n.static {\n position: static;\n}\n.fixed {\n position: fixed;\n}\n.absolute {\n position: absolute;\n}\n.relative {\n position: relative;\n}\n.sticky {\n position: sticky;\n}\n.inset-0 {\n inset: 0px;\n}\n.left-0 {\n left: 0px;\n}\n.right-0 {\n right: 0px;\n}\n.top-0 {\n top: 0px;\n}\n.top-8 {\n top: 2rem;\n}\n.isolate {\n isolation: isolate;\n}\n.z-\\[5\\] {\n z-index: 5;\n}\n.mb-0 {\n margin-bottom: 0px;\n}\n.mb-\\[1px\\] {\n margin-bottom: 1px;\n}\n.block {\n display: block;\n}\n.inline-block {\n display: inline-block;\n}\n.inline {\n display: inline;\n}\n.flex {\n display: flex;\n}\n.inline-flex {\n display: inline-flex;\n}\n.table {\n display: table;\n}\n.grid {\n display: grid;\n}\n.inline-grid {\n display: inline-grid;\n}\n.contents {\n display: contents;\n}\n.hidden {\n display: none;\n}\n.size-full {\n width: 100%;\n height: 100%;\n}\n.h-\\[1\\.1rem\\] {\n height: 1.1rem;\n}\n.h-\\[1080px\\] {\n height: 1080px;\n}\n.h-\\[500px\\] {\n height: 500px;\n}\n.h-\\[5px\\] {\n height: 5px;\n}\n.h-\\[calc\\(50vh-4rem\\)\\] {\n height: calc(50vh - 4rem);\n}\n.h-full {\n height: 100%;\n}\n.w-1 {\n width: 0.25rem;\n}\n.w-\\[1000px\\] {\n width: 1000px;\n}\n.w-\\[1920px\\] {\n width: 1920px;\n}\n.w-\\[420px\\] {\n width: 420px;\n}\n.w-full {\n width: 100%;\n}\n.flex-1 {\n flex: 1 1 0%;\n}\n.flex-shrink {\n flex-shrink: 1;\n}\n.shrink {\n flex-shrink: 1;\n}\n.grow {\n flex-grow: 1;\n}\n.\\!transform {\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)) !important;\n}\n.transform {\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n.resize {\n resize: both;\n}\n.flex-wrap {\n flex-wrap: wrap;\n}\n.items-center {\n align-items: center;\n}\n.overflow-hidden {\n overflow: hidden;\n}\n.overflow-visible {\n overflow: visible;\n}\n.whitespace-nowrap {\n white-space: nowrap;\n}\n.text-nowrap {\n text-wrap: nowrap;\n}\n.rounded {\n border-radius: 0.25rem;\n}\n.border {\n border-width: 1px;\n}\n.bg-slate-500 {\n --tw-bg-opacity: 1;\n background-color: rgb(100 116 139 / var(--tw-bg-opacity, 1));\n}\n.object-cover {\n -o-object-fit: cover;\n object-fit: cover;\n}\n.px-0\\.5 {\n padding-left: 0.125rem;\n padding-right: 0.125rem;\n}\n.text-center {\n text-align: center;\n}\n.text-3xl {\n font-size: 1.875rem;\n line-height: 2.25rem;\n}\n.text-\\[8px\\] {\n font-size: 8px;\n}\n.text-sm {\n font-size: 0.875rem;\n line-height: 1.25rem;\n}\n.text-xs {\n font-size: 0.75rem;\n line-height: 1rem;\n}\n.font-bold {\n font-weight: 700;\n}\n.uppercase {\n text-transform: uppercase;\n}\n.capitalize {\n text-transform: capitalize;\n}\n.italic {\n font-style: italic;\n}\n.text-white {\n --tw-text-opacity: 1;\n color: rgb(255 255 255 / var(--tw-text-opacity, 1));\n}\n.opacity-50 {\n opacity: 0.5;\n}\n.shadow {\n --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);\n --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);\n box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);\n}\n.outline {\n outline-style: solid;\n}\n.blur {\n --tw-blur: blur(8px);\n filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);\n}\n.grayscale {\n --tw-grayscale: grayscale(100%);\n filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);\n}\n.filter {\n filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);\n}\n.backdrop-filter {\n backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);\n}\n.transition {\n transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;\n transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n transition-duration: 150ms;\n}\n.ease-in {\n transition-timing-function: cubic-bezier(0.4, 0, 1, 1);\n}\n.ease-in-out {\n transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n}\n.ease-out {\n transition-timing-function: cubic-bezier(0, 0, 0.2, 1);\n}\n";
3
3
 
4
4
  //#endregion
5
5
  export { TWMixin_default as default };
@@ -1 +1 @@
1
- {"version":3,"file":"TWMixin.js","names":[],"sources":["../../src/gui/TWMixin.css?inline"],"sourcesContent":["export default \"/* biome-ignore lint/suspicious/noUnknownAtRules: @tailwind is a valid Tailwind CSS directive */\\n*, ::before, ::after {\\n --tw-border-spacing-x: 0;\\n --tw-border-spacing-y: 0;\\n --tw-translate-x: 0;\\n --tw-translate-y: 0;\\n --tw-rotate: 0;\\n --tw-skew-x: 0;\\n --tw-skew-y: 0;\\n --tw-scale-x: 1;\\n --tw-scale-y: 1;\\n --tw-pan-x: ;\\n --tw-pan-y: ;\\n --tw-pinch-zoom: ;\\n --tw-scroll-snap-strictness: proximity;\\n --tw-gradient-from-position: ;\\n --tw-gradient-via-position: ;\\n --tw-gradient-to-position: ;\\n --tw-ordinal: ;\\n --tw-slashed-zero: ;\\n --tw-numeric-figure: ;\\n --tw-numeric-spacing: ;\\n --tw-numeric-fraction: ;\\n --tw-ring-inset: ;\\n --tw-ring-offset-width: 0px;\\n --tw-ring-offset-color: #fff;\\n --tw-ring-color: rgb(59 130 246 / 0.5);\\n --tw-ring-offset-shadow: 0 0 #0000;\\n --tw-ring-shadow: 0 0 #0000;\\n --tw-shadow: 0 0 #0000;\\n --tw-shadow-colored: 0 0 #0000;\\n --tw-blur: ;\\n --tw-brightness: ;\\n --tw-contrast: ;\\n --tw-grayscale: ;\\n --tw-hue-rotate: ;\\n --tw-invert: ;\\n --tw-saturate: ;\\n --tw-sepia: ;\\n --tw-drop-shadow: ;\\n --tw-backdrop-blur: ;\\n --tw-backdrop-brightness: ;\\n --tw-backdrop-contrast: ;\\n --tw-backdrop-grayscale: ;\\n --tw-backdrop-hue-rotate: ;\\n --tw-backdrop-invert: ;\\n --tw-backdrop-opacity: ;\\n --tw-backdrop-saturate: ;\\n --tw-backdrop-sepia: ;\\n --tw-contain-size: ;\\n --tw-contain-layout: ;\\n --tw-contain-paint: ;\\n --tw-contain-style: ;\\n}\\n::backdrop {\\n --tw-border-spacing-x: 0;\\n --tw-border-spacing-y: 0;\\n --tw-translate-x: 0;\\n --tw-translate-y: 0;\\n --tw-rotate: 0;\\n --tw-skew-x: 0;\\n --tw-skew-y: 0;\\n --tw-scale-x: 1;\\n --tw-scale-y: 1;\\n --tw-pan-x: ;\\n --tw-pan-y: ;\\n --tw-pinch-zoom: ;\\n --tw-scroll-snap-strictness: proximity;\\n --tw-gradient-from-position: ;\\n --tw-gradient-via-position: ;\\n --tw-gradient-to-position: ;\\n --tw-ordinal: ;\\n --tw-slashed-zero: ;\\n --tw-numeric-figure: ;\\n --tw-numeric-spacing: ;\\n --tw-numeric-fraction: ;\\n --tw-ring-inset: ;\\n --tw-ring-offset-width: 0px;\\n --tw-ring-offset-color: #fff;\\n --tw-ring-color: rgb(59 130 246 / 0.5);\\n --tw-ring-offset-shadow: 0 0 #0000;\\n --tw-ring-shadow: 0 0 #0000;\\n --tw-shadow: 0 0 #0000;\\n --tw-shadow-colored: 0 0 #0000;\\n --tw-blur: ;\\n --tw-brightness: ;\\n --tw-contrast: ;\\n --tw-grayscale: ;\\n --tw-hue-rotate: ;\\n --tw-invert: ;\\n --tw-saturate: ;\\n --tw-sepia: ;\\n --tw-drop-shadow: ;\\n --tw-backdrop-blur: ;\\n --tw-backdrop-brightness: ;\\n --tw-backdrop-contrast: ;\\n --tw-backdrop-grayscale: ;\\n --tw-backdrop-hue-rotate: ;\\n --tw-backdrop-invert: ;\\n --tw-backdrop-opacity: ;\\n --tw-backdrop-saturate: ;\\n --tw-backdrop-sepia: ;\\n --tw-contain-size: ;\\n --tw-contain-layout: ;\\n --tw-contain-paint: ;\\n --tw-contain-style: ;\\n}\\n/* ! tailwindcss v3.4.18 | MIT License | https://tailwindcss.com */\\n/*\\n1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)\\n2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)\\n*/\\n*,\\n::before,\\n::after {\\n box-sizing: border-box; /* 1 */\\n border-width: 0; /* 2 */\\n border-style: solid; /* 2 */\\n border-color: #e5e7eb; /* 2 */\\n}\\n::before,\\n::after {\\n --tw-content: '';\\n}\\n/*\\n1. Use a consistent sensible line-height in all browsers.\\n2. Prevent adjustments of font size after orientation changes in iOS.\\n3. Use a more readable tab size.\\n4. Use the user's configured `sans` font-family by default.\\n5. Use the user's configured `sans` font-feature-settings by default.\\n6. Use the user's configured `sans` font-variation-settings by default.\\n7. Disable tap highlights on iOS\\n*/\\nhtml,\\n:host {\\n line-height: 1.5; /* 1 */\\n -webkit-text-size-adjust: 100%; /* 2 */\\n -moz-tab-size: 4; /* 3 */\\n -o-tab-size: 4;\\n tab-size: 4; /* 3 */\\n font-family: ui-sans-serif, system-ui, sans-serif, \\\"Apple Color Emoji\\\", \\\"Segoe UI Emoji\\\", \\\"Segoe UI Symbol\\\", \\\"Noto Color Emoji\\\"; /* 4 */\\n font-feature-settings: normal; /* 5 */\\n font-variation-settings: normal; /* 6 */\\n -webkit-tap-highlight-color: transparent; /* 7 */\\n}\\n/*\\n1. Remove the margin in all browsers.\\n2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.\\n*/\\nbody {\\n margin: 0; /* 1 */\\n line-height: inherit; /* 2 */\\n}\\n/*\\n1. Add the correct height in Firefox.\\n2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)\\n3. Ensure horizontal rules are visible by default.\\n*/\\nhr {\\n height: 0; /* 1 */\\n color: inherit; /* 2 */\\n border-top-width: 1px; /* 3 */\\n}\\n/*\\nAdd the correct text decoration in Chrome, Edge, and Safari.\\n*/\\nabbr:where([title]) {\\n -webkit-text-decoration: underline dotted;\\n text-decoration: underline dotted;\\n}\\n/*\\nRemove the default font size and weight for headings.\\n*/\\nh1,\\nh2,\\nh3,\\nh4,\\nh5,\\nh6 {\\n font-size: inherit;\\n font-weight: inherit;\\n}\\n/*\\nReset links to optimize for opt-in styling instead of opt-out.\\n*/\\na {\\n color: inherit;\\n text-decoration: inherit;\\n}\\n/*\\nAdd the correct font weight in Edge and Safari.\\n*/\\nb,\\nstrong {\\n font-weight: bolder;\\n}\\n/*\\n1. Use the user's configured `mono` font-family by default.\\n2. Use the user's configured `mono` font-feature-settings by default.\\n3. Use the user's configured `mono` font-variation-settings by default.\\n4. Correct the odd `em` font sizing in all browsers.\\n*/\\ncode,\\nkbd,\\nsamp,\\npre {\\n font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \\\"Liberation Mono\\\", \\\"Courier New\\\", monospace; /* 1 */\\n font-feature-settings: normal; /* 2 */\\n font-variation-settings: normal; /* 3 */\\n font-size: 1em; /* 4 */\\n}\\n/*\\nAdd the correct font size in all browsers.\\n*/\\nsmall {\\n font-size: 80%;\\n}\\n/*\\nPrevent `sub` and `sup` elements from affecting the line height in all browsers.\\n*/\\nsub,\\nsup {\\n font-size: 75%;\\n line-height: 0;\\n position: relative;\\n vertical-align: baseline;\\n}\\nsub {\\n bottom: -0.25em;\\n}\\nsup {\\n top: -0.5em;\\n}\\n/*\\n1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)\\n2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)\\n3. Remove gaps between table borders by default.\\n*/\\ntable {\\n text-indent: 0; /* 1 */\\n border-color: inherit; /* 2 */\\n border-collapse: collapse; /* 3 */\\n}\\n/*\\n1. Change the font styles in all browsers.\\n2. Remove the margin in Firefox and Safari.\\n3. Remove default padding in all browsers.\\n*/\\nbutton,\\ninput,\\noptgroup,\\nselect,\\ntextarea {\\n font-family: inherit; /* 1 */\\n font-feature-settings: inherit; /* 1 */\\n font-variation-settings: inherit; /* 1 */\\n font-size: 100%; /* 1 */\\n font-weight: inherit; /* 1 */\\n line-height: inherit; /* 1 */\\n letter-spacing: inherit; /* 1 */\\n color: inherit; /* 1 */\\n margin: 0; /* 2 */\\n padding: 0; /* 3 */\\n}\\n/*\\nRemove the inheritance of text transform in Edge and Firefox.\\n*/\\nbutton,\\nselect {\\n text-transform: none;\\n}\\n/*\\n1. Correct the inability to style clickable types in iOS and Safari.\\n2. Remove default button styles.\\n*/\\nbutton,\\ninput:where([type='button']),\\ninput:where([type='reset']),\\ninput:where([type='submit']) {\\n -webkit-appearance: button; /* 1 */\\n background-color: transparent; /* 2 */\\n background-image: none; /* 2 */\\n}\\n/*\\nUse the modern Firefox focus style for all focusable elements.\\n*/\\n:-moz-focusring {\\n outline: auto;\\n}\\n/*\\nRemove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)\\n*/\\n:-moz-ui-invalid {\\n box-shadow: none;\\n}\\n/*\\nAdd the correct vertical alignment in Chrome and Firefox.\\n*/\\nprogress {\\n vertical-align: baseline;\\n}\\n/*\\nCorrect the cursor style of increment and decrement buttons in Safari.\\n*/\\n::-webkit-inner-spin-button,\\n::-webkit-outer-spin-button {\\n height: auto;\\n}\\n/*\\n1. Correct the odd appearance in Chrome and Safari.\\n2. Correct the outline style in Safari.\\n*/\\n[type='search'] {\\n -webkit-appearance: textfield; /* 1 */\\n outline-offset: -2px; /* 2 */\\n}\\n/*\\nRemove the inner padding in Chrome and Safari on macOS.\\n*/\\n::-webkit-search-decoration {\\n -webkit-appearance: none;\\n}\\n/*\\n1. Correct the inability to style clickable types in iOS and Safari.\\n2. Change font properties to `inherit` in Safari.\\n*/\\n::-webkit-file-upload-button {\\n -webkit-appearance: button; /* 1 */\\n font: inherit; /* 2 */\\n}\\n/*\\nAdd the correct display in Chrome and Safari.\\n*/\\nsummary {\\n display: list-item;\\n}\\n/*\\nRemoves the default spacing and border for appropriate elements.\\n*/\\nblockquote,\\ndl,\\ndd,\\nh1,\\nh2,\\nh3,\\nh4,\\nh5,\\nh6,\\nhr,\\nfigure,\\np,\\npre {\\n margin: 0;\\n}\\nfieldset {\\n margin: 0;\\n padding: 0;\\n}\\nlegend {\\n padding: 0;\\n}\\nol,\\nul,\\nmenu {\\n list-style: none;\\n margin: 0;\\n padding: 0;\\n}\\n/*\\nReset default styling for dialogs.\\n*/\\ndialog {\\n padding: 0;\\n}\\n/*\\nPrevent resizing textareas horizontally by default.\\n*/\\ntextarea {\\n resize: vertical;\\n}\\n/*\\n1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)\\n2. Set the default placeholder color to the user's configured gray 400 color.\\n*/\\ninput::-moz-placeholder, textarea::-moz-placeholder {\\n opacity: 1; /* 1 */\\n color: #9ca3af; /* 2 */\\n}\\ninput::placeholder,\\ntextarea::placeholder {\\n opacity: 1; /* 1 */\\n color: #9ca3af; /* 2 */\\n}\\n/*\\nSet the default cursor for buttons.\\n*/\\nbutton,\\n[role=\\\"button\\\"] {\\n cursor: pointer;\\n}\\n/*\\nMake sure disabled buttons don't get the pointer cursor.\\n*/\\n:disabled {\\n cursor: default;\\n}\\n/*\\n1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)\\n2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)\\n This can trigger a poorly considered lint error in some tools but is included by design.\\n*/\\nimg,\\nsvg,\\nvideo,\\ncanvas,\\naudio,\\niframe,\\nembed,\\nobject {\\n display: block; /* 1 */\\n vertical-align: middle; /* 2 */\\n}\\n/*\\nConstrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)\\n*/\\nimg,\\nvideo {\\n max-width: 100%;\\n height: auto;\\n}\\n/* Make elements with the HTML hidden attribute stay hidden by default */\\n[hidden]:where(:not([hidden=\\\"until-found\\\"])) {\\n display: none;\\n}\\n/* biome-ignore lint/suspicious/noUnknownAtRules: @tailwind is a valid Tailwind CSS directive */\\n.\\\\!container {\\n width: 100% !important;\\n}\\n.container {\\n width: 100%;\\n}\\n@media (min-width: 640px) {\\n .\\\\!container {\\n max-width: 640px !important;\\n }\\n .container {\\n max-width: 640px;\\n }\\n}\\n@media (min-width: 768px) {\\n .\\\\!container {\\n max-width: 768px !important;\\n }\\n .container {\\n max-width: 768px;\\n }\\n}\\n@media (min-width: 1024px) {\\n .\\\\!container {\\n max-width: 1024px !important;\\n }\\n .container {\\n max-width: 1024px;\\n }\\n}\\n@media (min-width: 1280px) {\\n .\\\\!container {\\n max-width: 1280px !important;\\n }\\n .container {\\n max-width: 1280px;\\n }\\n}\\n@media (min-width: 1536px) {\\n .\\\\!container {\\n max-width: 1536px !important;\\n }\\n .container {\\n max-width: 1536px;\\n }\\n}\\n/* biome-ignore lint/suspicious/noUnknownAtRules: @tailwind is a valid Tailwind CSS directive */\\n.visible {\\n visibility: visible;\\n}\\n.invisible {\\n visibility: hidden;\\n}\\n.collapse {\\n visibility: collapse;\\n}\\n.static {\\n position: static;\\n}\\n.fixed {\\n position: fixed;\\n}\\n.absolute {\\n position: absolute;\\n}\\n.relative {\\n position: relative;\\n}\\n.sticky {\\n position: sticky;\\n}\\n.inset-0 {\\n inset: 0px;\\n}\\n.left-0 {\\n left: 0px;\\n}\\n.right-0 {\\n right: 0px;\\n}\\n.top-0 {\\n top: 0px;\\n}\\n.top-8 {\\n top: 2rem;\\n}\\n.isolate {\\n isolation: isolate;\\n}\\n.z-\\\\[5\\\\] {\\n z-index: 5;\\n}\\n.mb-0 {\\n margin-bottom: 0px;\\n}\\n.mb-\\\\[1px\\\\] {\\n margin-bottom: 1px;\\n}\\n.block {\\n display: block;\\n}\\n.inline-block {\\n display: inline-block;\\n}\\n.inline {\\n display: inline;\\n}\\n.flex {\\n display: flex;\\n}\\n.inline-flex {\\n display: inline-flex;\\n}\\n.table {\\n display: table;\\n}\\n.grid {\\n display: grid;\\n}\\n.inline-grid {\\n display: inline-grid;\\n}\\n.contents {\\n display: contents;\\n}\\n.hidden {\\n display: none;\\n}\\n.size-full {\\n width: 100%;\\n height: 100%;\\n}\\n.h-\\\\[1\\\\.1rem\\\\] {\\n height: 1.1rem;\\n}\\n.h-\\\\[1080px\\\\] {\\n height: 1080px;\\n}\\n.h-\\\\[500px\\\\] {\\n height: 500px;\\n}\\n.h-\\\\[5px\\\\] {\\n height: 5px;\\n}\\n.h-\\\\[calc\\\\(50vh-4rem\\\\)\\\\] {\\n height: calc(50vh - 4rem);\\n}\\n.h-full {\\n height: 100%;\\n}\\n.w-1 {\\n width: 0.25rem;\\n}\\n.w-\\\\[1000px\\\\] {\\n width: 1000px;\\n}\\n.w-\\\\[1920px\\\\] {\\n width: 1920px;\\n}\\n.w-\\\\[420px\\\\] {\\n width: 420px;\\n}\\n.w-full {\\n width: 100%;\\n}\\n.flex-1 {\\n flex: 1 1 0%;\\n}\\n.flex-shrink {\\n flex-shrink: 1;\\n}\\n.shrink {\\n flex-shrink: 1;\\n}\\n.\\\\!transform {\\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)) !important;\\n}\\n.transform {\\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\\n}\\n.resize {\\n resize: both;\\n}\\n.flex-wrap {\\n flex-wrap: wrap;\\n}\\n.items-center {\\n align-items: center;\\n}\\n.overflow-hidden {\\n overflow: hidden;\\n}\\n.overflow-visible {\\n overflow: visible;\\n}\\n.whitespace-nowrap {\\n white-space: nowrap;\\n}\\n.text-nowrap {\\n text-wrap: nowrap;\\n}\\n.rounded {\\n border-radius: 0.25rem;\\n}\\n.border {\\n border-width: 1px;\\n}\\n.bg-slate-500 {\\n --tw-bg-opacity: 1;\\n background-color: rgb(100 116 139 / var(--tw-bg-opacity, 1));\\n}\\n.object-cover {\\n -o-object-fit: cover;\\n object-fit: cover;\\n}\\n.px-0\\\\.5 {\\n padding-left: 0.125rem;\\n padding-right: 0.125rem;\\n}\\n.text-center {\\n text-align: center;\\n}\\n.text-3xl {\\n font-size: 1.875rem;\\n line-height: 2.25rem;\\n}\\n.text-\\\\[8px\\\\] {\\n font-size: 8px;\\n}\\n.text-sm {\\n font-size: 0.875rem;\\n line-height: 1.25rem;\\n}\\n.text-xs {\\n font-size: 0.75rem;\\n line-height: 1rem;\\n}\\n.font-bold {\\n font-weight: 700;\\n}\\n.uppercase {\\n text-transform: uppercase;\\n}\\n.capitalize {\\n text-transform: capitalize;\\n}\\n.italic {\\n font-style: italic;\\n}\\n.text-white {\\n --tw-text-opacity: 1;\\n color: rgb(255 255 255 / var(--tw-text-opacity, 1));\\n}\\n.opacity-50 {\\n opacity: 0.5;\\n}\\n.shadow {\\n --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);\\n --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);\\n box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);\\n}\\n.outline {\\n outline-style: solid;\\n}\\n.blur {\\n --tw-blur: blur(8px);\\n filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);\\n}\\n.grayscale {\\n --tw-grayscale: grayscale(100%);\\n filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);\\n}\\n.filter {\\n filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);\\n}\\n.backdrop-filter {\\n backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);\\n}\\n.transition {\\n transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;\\n transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\\n transition-duration: 150ms;\\n}\\n.ease-in {\\n transition-timing-function: cubic-bezier(0.4, 0, 1, 1);\\n}\\n.ease-in-out {\\n transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\\n}\\n.ease-out {\\n transition-timing-function: cubic-bezier(0, 0, 0.2, 1);\\n}\\n\""],"mappings":";AAAA,sBAAe"}
1
+ {"version":3,"file":"TWMixin.js","names":[],"sources":["../../src/gui/TWMixin.css?inline"],"sourcesContent":["export default \"/* biome-ignore lint/suspicious/noUnknownAtRules: @tailwind is a valid Tailwind CSS directive */\\n*, ::before, ::after {\\n --tw-border-spacing-x: 0;\\n --tw-border-spacing-y: 0;\\n --tw-translate-x: 0;\\n --tw-translate-y: 0;\\n --tw-rotate: 0;\\n --tw-skew-x: 0;\\n --tw-skew-y: 0;\\n --tw-scale-x: 1;\\n --tw-scale-y: 1;\\n --tw-pan-x: ;\\n --tw-pan-y: ;\\n --tw-pinch-zoom: ;\\n --tw-scroll-snap-strictness: proximity;\\n --tw-gradient-from-position: ;\\n --tw-gradient-via-position: ;\\n --tw-gradient-to-position: ;\\n --tw-ordinal: ;\\n --tw-slashed-zero: ;\\n --tw-numeric-figure: ;\\n --tw-numeric-spacing: ;\\n --tw-numeric-fraction: ;\\n --tw-ring-inset: ;\\n --tw-ring-offset-width: 0px;\\n --tw-ring-offset-color: #fff;\\n --tw-ring-color: rgb(59 130 246 / 0.5);\\n --tw-ring-offset-shadow: 0 0 #0000;\\n --tw-ring-shadow: 0 0 #0000;\\n --tw-shadow: 0 0 #0000;\\n --tw-shadow-colored: 0 0 #0000;\\n --tw-blur: ;\\n --tw-brightness: ;\\n --tw-contrast: ;\\n --tw-grayscale: ;\\n --tw-hue-rotate: ;\\n --tw-invert: ;\\n --tw-saturate: ;\\n --tw-sepia: ;\\n --tw-drop-shadow: ;\\n --tw-backdrop-blur: ;\\n --tw-backdrop-brightness: ;\\n --tw-backdrop-contrast: ;\\n --tw-backdrop-grayscale: ;\\n --tw-backdrop-hue-rotate: ;\\n --tw-backdrop-invert: ;\\n --tw-backdrop-opacity: ;\\n --tw-backdrop-saturate: ;\\n --tw-backdrop-sepia: ;\\n --tw-contain-size: ;\\n --tw-contain-layout: ;\\n --tw-contain-paint: ;\\n --tw-contain-style: ;\\n}\\n::backdrop {\\n --tw-border-spacing-x: 0;\\n --tw-border-spacing-y: 0;\\n --tw-translate-x: 0;\\n --tw-translate-y: 0;\\n --tw-rotate: 0;\\n --tw-skew-x: 0;\\n --tw-skew-y: 0;\\n --tw-scale-x: 1;\\n --tw-scale-y: 1;\\n --tw-pan-x: ;\\n --tw-pan-y: ;\\n --tw-pinch-zoom: ;\\n --tw-scroll-snap-strictness: proximity;\\n --tw-gradient-from-position: ;\\n --tw-gradient-via-position: ;\\n --tw-gradient-to-position: ;\\n --tw-ordinal: ;\\n --tw-slashed-zero: ;\\n --tw-numeric-figure: ;\\n --tw-numeric-spacing: ;\\n --tw-numeric-fraction: ;\\n --tw-ring-inset: ;\\n --tw-ring-offset-width: 0px;\\n --tw-ring-offset-color: #fff;\\n --tw-ring-color: rgb(59 130 246 / 0.5);\\n --tw-ring-offset-shadow: 0 0 #0000;\\n --tw-ring-shadow: 0 0 #0000;\\n --tw-shadow: 0 0 #0000;\\n --tw-shadow-colored: 0 0 #0000;\\n --tw-blur: ;\\n --tw-brightness: ;\\n --tw-contrast: ;\\n --tw-grayscale: ;\\n --tw-hue-rotate: ;\\n --tw-invert: ;\\n --tw-saturate: ;\\n --tw-sepia: ;\\n --tw-drop-shadow: ;\\n --tw-backdrop-blur: ;\\n --tw-backdrop-brightness: ;\\n --tw-backdrop-contrast: ;\\n --tw-backdrop-grayscale: ;\\n --tw-backdrop-hue-rotate: ;\\n --tw-backdrop-invert: ;\\n --tw-backdrop-opacity: ;\\n --tw-backdrop-saturate: ;\\n --tw-backdrop-sepia: ;\\n --tw-contain-size: ;\\n --tw-contain-layout: ;\\n --tw-contain-paint: ;\\n --tw-contain-style: ;\\n}\\n/* ! tailwindcss v3.4.18 | MIT License | https://tailwindcss.com */\\n/*\\n1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)\\n2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)\\n*/\\n*,\\n::before,\\n::after {\\n box-sizing: border-box; /* 1 */\\n border-width: 0; /* 2 */\\n border-style: solid; /* 2 */\\n border-color: #e5e7eb; /* 2 */\\n}\\n::before,\\n::after {\\n --tw-content: '';\\n}\\n/*\\n1. Use a consistent sensible line-height in all browsers.\\n2. Prevent adjustments of font size after orientation changes in iOS.\\n3. Use a more readable tab size.\\n4. Use the user's configured `sans` font-family by default.\\n5. Use the user's configured `sans` font-feature-settings by default.\\n6. Use the user's configured `sans` font-variation-settings by default.\\n7. Disable tap highlights on iOS\\n*/\\nhtml,\\n:host {\\n line-height: 1.5; /* 1 */\\n -webkit-text-size-adjust: 100%; /* 2 */\\n -moz-tab-size: 4; /* 3 */\\n -o-tab-size: 4;\\n tab-size: 4; /* 3 */\\n font-family: ui-sans-serif, system-ui, sans-serif, \\\"Apple Color Emoji\\\", \\\"Segoe UI Emoji\\\", \\\"Segoe UI Symbol\\\", \\\"Noto Color Emoji\\\"; /* 4 */\\n font-feature-settings: normal; /* 5 */\\n font-variation-settings: normal; /* 6 */\\n -webkit-tap-highlight-color: transparent; /* 7 */\\n}\\n/*\\n1. Remove the margin in all browsers.\\n2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.\\n*/\\nbody {\\n margin: 0; /* 1 */\\n line-height: inherit; /* 2 */\\n}\\n/*\\n1. Add the correct height in Firefox.\\n2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)\\n3. Ensure horizontal rules are visible by default.\\n*/\\nhr {\\n height: 0; /* 1 */\\n color: inherit; /* 2 */\\n border-top-width: 1px; /* 3 */\\n}\\n/*\\nAdd the correct text decoration in Chrome, Edge, and Safari.\\n*/\\nabbr:where([title]) {\\n -webkit-text-decoration: underline dotted;\\n text-decoration: underline dotted;\\n}\\n/*\\nRemove the default font size and weight for headings.\\n*/\\nh1,\\nh2,\\nh3,\\nh4,\\nh5,\\nh6 {\\n font-size: inherit;\\n font-weight: inherit;\\n}\\n/*\\nReset links to optimize for opt-in styling instead of opt-out.\\n*/\\na {\\n color: inherit;\\n text-decoration: inherit;\\n}\\n/*\\nAdd the correct font weight in Edge and Safari.\\n*/\\nb,\\nstrong {\\n font-weight: bolder;\\n}\\n/*\\n1. Use the user's configured `mono` font-family by default.\\n2. Use the user's configured `mono` font-feature-settings by default.\\n3. Use the user's configured `mono` font-variation-settings by default.\\n4. Correct the odd `em` font sizing in all browsers.\\n*/\\ncode,\\nkbd,\\nsamp,\\npre {\\n font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \\\"Liberation Mono\\\", \\\"Courier New\\\", monospace; /* 1 */\\n font-feature-settings: normal; /* 2 */\\n font-variation-settings: normal; /* 3 */\\n font-size: 1em; /* 4 */\\n}\\n/*\\nAdd the correct font size in all browsers.\\n*/\\nsmall {\\n font-size: 80%;\\n}\\n/*\\nPrevent `sub` and `sup` elements from affecting the line height in all browsers.\\n*/\\nsub,\\nsup {\\n font-size: 75%;\\n line-height: 0;\\n position: relative;\\n vertical-align: baseline;\\n}\\nsub {\\n bottom: -0.25em;\\n}\\nsup {\\n top: -0.5em;\\n}\\n/*\\n1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)\\n2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)\\n3. Remove gaps between table borders by default.\\n*/\\ntable {\\n text-indent: 0; /* 1 */\\n border-color: inherit; /* 2 */\\n border-collapse: collapse; /* 3 */\\n}\\n/*\\n1. Change the font styles in all browsers.\\n2. Remove the margin in Firefox and Safari.\\n3. Remove default padding in all browsers.\\n*/\\nbutton,\\ninput,\\noptgroup,\\nselect,\\ntextarea {\\n font-family: inherit; /* 1 */\\n font-feature-settings: inherit; /* 1 */\\n font-variation-settings: inherit; /* 1 */\\n font-size: 100%; /* 1 */\\n font-weight: inherit; /* 1 */\\n line-height: inherit; /* 1 */\\n letter-spacing: inherit; /* 1 */\\n color: inherit; /* 1 */\\n margin: 0; /* 2 */\\n padding: 0; /* 3 */\\n}\\n/*\\nRemove the inheritance of text transform in Edge and Firefox.\\n*/\\nbutton,\\nselect {\\n text-transform: none;\\n}\\n/*\\n1. Correct the inability to style clickable types in iOS and Safari.\\n2. Remove default button styles.\\n*/\\nbutton,\\ninput:where([type='button']),\\ninput:where([type='reset']),\\ninput:where([type='submit']) {\\n -webkit-appearance: button; /* 1 */\\n background-color: transparent; /* 2 */\\n background-image: none; /* 2 */\\n}\\n/*\\nUse the modern Firefox focus style for all focusable elements.\\n*/\\n:-moz-focusring {\\n outline: auto;\\n}\\n/*\\nRemove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)\\n*/\\n:-moz-ui-invalid {\\n box-shadow: none;\\n}\\n/*\\nAdd the correct vertical alignment in Chrome and Firefox.\\n*/\\nprogress {\\n vertical-align: baseline;\\n}\\n/*\\nCorrect the cursor style of increment and decrement buttons in Safari.\\n*/\\n::-webkit-inner-spin-button,\\n::-webkit-outer-spin-button {\\n height: auto;\\n}\\n/*\\n1. Correct the odd appearance in Chrome and Safari.\\n2. Correct the outline style in Safari.\\n*/\\n[type='search'] {\\n -webkit-appearance: textfield; /* 1 */\\n outline-offset: -2px; /* 2 */\\n}\\n/*\\nRemove the inner padding in Chrome and Safari on macOS.\\n*/\\n::-webkit-search-decoration {\\n -webkit-appearance: none;\\n}\\n/*\\n1. Correct the inability to style clickable types in iOS and Safari.\\n2. Change font properties to `inherit` in Safari.\\n*/\\n::-webkit-file-upload-button {\\n -webkit-appearance: button; /* 1 */\\n font: inherit; /* 2 */\\n}\\n/*\\nAdd the correct display in Chrome and Safari.\\n*/\\nsummary {\\n display: list-item;\\n}\\n/*\\nRemoves the default spacing and border for appropriate elements.\\n*/\\nblockquote,\\ndl,\\ndd,\\nh1,\\nh2,\\nh3,\\nh4,\\nh5,\\nh6,\\nhr,\\nfigure,\\np,\\npre {\\n margin: 0;\\n}\\nfieldset {\\n margin: 0;\\n padding: 0;\\n}\\nlegend {\\n padding: 0;\\n}\\nol,\\nul,\\nmenu {\\n list-style: none;\\n margin: 0;\\n padding: 0;\\n}\\n/*\\nReset default styling for dialogs.\\n*/\\ndialog {\\n padding: 0;\\n}\\n/*\\nPrevent resizing textareas horizontally by default.\\n*/\\ntextarea {\\n resize: vertical;\\n}\\n/*\\n1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)\\n2. Set the default placeholder color to the user's configured gray 400 color.\\n*/\\ninput::-moz-placeholder, textarea::-moz-placeholder {\\n opacity: 1; /* 1 */\\n color: #9ca3af; /* 2 */\\n}\\ninput::placeholder,\\ntextarea::placeholder {\\n opacity: 1; /* 1 */\\n color: #9ca3af; /* 2 */\\n}\\n/*\\nSet the default cursor for buttons.\\n*/\\nbutton,\\n[role=\\\"button\\\"] {\\n cursor: pointer;\\n}\\n/*\\nMake sure disabled buttons don't get the pointer cursor.\\n*/\\n:disabled {\\n cursor: default;\\n}\\n/*\\n1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)\\n2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)\\n This can trigger a poorly considered lint error in some tools but is included by design.\\n*/\\nimg,\\nsvg,\\nvideo,\\ncanvas,\\naudio,\\niframe,\\nembed,\\nobject {\\n display: block; /* 1 */\\n vertical-align: middle; /* 2 */\\n}\\n/*\\nConstrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)\\n*/\\nimg,\\nvideo {\\n max-width: 100%;\\n height: auto;\\n}\\n/* Make elements with the HTML hidden attribute stay hidden by default */\\n[hidden]:where(:not([hidden=\\\"until-found\\\"])) {\\n display: none;\\n}\\n/* biome-ignore lint/suspicious/noUnknownAtRules: @tailwind is a valid Tailwind CSS directive */\\n.\\\\!container {\\n width: 100% !important;\\n}\\n.container {\\n width: 100%;\\n}\\n@media (min-width: 640px) {\\n .\\\\!container {\\n max-width: 640px !important;\\n }\\n .container {\\n max-width: 640px;\\n }\\n}\\n@media (min-width: 768px) {\\n .\\\\!container {\\n max-width: 768px !important;\\n }\\n .container {\\n max-width: 768px;\\n }\\n}\\n@media (min-width: 1024px) {\\n .\\\\!container {\\n max-width: 1024px !important;\\n }\\n .container {\\n max-width: 1024px;\\n }\\n}\\n@media (min-width: 1280px) {\\n .\\\\!container {\\n max-width: 1280px !important;\\n }\\n .container {\\n max-width: 1280px;\\n }\\n}\\n@media (min-width: 1536px) {\\n .\\\\!container {\\n max-width: 1536px !important;\\n }\\n .container {\\n max-width: 1536px;\\n }\\n}\\n/* biome-ignore lint/suspicious/noUnknownAtRules: @tailwind is a valid Tailwind CSS directive */\\n.visible {\\n visibility: visible;\\n}\\n.invisible {\\n visibility: hidden;\\n}\\n.collapse {\\n visibility: collapse;\\n}\\n.static {\\n position: static;\\n}\\n.fixed {\\n position: fixed;\\n}\\n.absolute {\\n position: absolute;\\n}\\n.relative {\\n position: relative;\\n}\\n.sticky {\\n position: sticky;\\n}\\n.inset-0 {\\n inset: 0px;\\n}\\n.left-0 {\\n left: 0px;\\n}\\n.right-0 {\\n right: 0px;\\n}\\n.top-0 {\\n top: 0px;\\n}\\n.top-8 {\\n top: 2rem;\\n}\\n.isolate {\\n isolation: isolate;\\n}\\n.z-\\\\[5\\\\] {\\n z-index: 5;\\n}\\n.mb-0 {\\n margin-bottom: 0px;\\n}\\n.mb-\\\\[1px\\\\] {\\n margin-bottom: 1px;\\n}\\n.block {\\n display: block;\\n}\\n.inline-block {\\n display: inline-block;\\n}\\n.inline {\\n display: inline;\\n}\\n.flex {\\n display: flex;\\n}\\n.inline-flex {\\n display: inline-flex;\\n}\\n.table {\\n display: table;\\n}\\n.grid {\\n display: grid;\\n}\\n.inline-grid {\\n display: inline-grid;\\n}\\n.contents {\\n display: contents;\\n}\\n.hidden {\\n display: none;\\n}\\n.size-full {\\n width: 100%;\\n height: 100%;\\n}\\n.h-\\\\[1\\\\.1rem\\\\] {\\n height: 1.1rem;\\n}\\n.h-\\\\[1080px\\\\] {\\n height: 1080px;\\n}\\n.h-\\\\[500px\\\\] {\\n height: 500px;\\n}\\n.h-\\\\[5px\\\\] {\\n height: 5px;\\n}\\n.h-\\\\[calc\\\\(50vh-4rem\\\\)\\\\] {\\n height: calc(50vh - 4rem);\\n}\\n.h-full {\\n height: 100%;\\n}\\n.w-1 {\\n width: 0.25rem;\\n}\\n.w-\\\\[1000px\\\\] {\\n width: 1000px;\\n}\\n.w-\\\\[1920px\\\\] {\\n width: 1920px;\\n}\\n.w-\\\\[420px\\\\] {\\n width: 420px;\\n}\\n.w-full {\\n width: 100%;\\n}\\n.flex-1 {\\n flex: 1 1 0%;\\n}\\n.flex-shrink {\\n flex-shrink: 1;\\n}\\n.shrink {\\n flex-shrink: 1;\\n}\\n.grow {\\n flex-grow: 1;\\n}\\n.\\\\!transform {\\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)) !important;\\n}\\n.transform {\\n transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\\n}\\n.resize {\\n resize: both;\\n}\\n.flex-wrap {\\n flex-wrap: wrap;\\n}\\n.items-center {\\n align-items: center;\\n}\\n.overflow-hidden {\\n overflow: hidden;\\n}\\n.overflow-visible {\\n overflow: visible;\\n}\\n.whitespace-nowrap {\\n white-space: nowrap;\\n}\\n.text-nowrap {\\n text-wrap: nowrap;\\n}\\n.rounded {\\n border-radius: 0.25rem;\\n}\\n.border {\\n border-width: 1px;\\n}\\n.bg-slate-500 {\\n --tw-bg-opacity: 1;\\n background-color: rgb(100 116 139 / var(--tw-bg-opacity, 1));\\n}\\n.object-cover {\\n -o-object-fit: cover;\\n object-fit: cover;\\n}\\n.px-0\\\\.5 {\\n padding-left: 0.125rem;\\n padding-right: 0.125rem;\\n}\\n.text-center {\\n text-align: center;\\n}\\n.text-3xl {\\n font-size: 1.875rem;\\n line-height: 2.25rem;\\n}\\n.text-\\\\[8px\\\\] {\\n font-size: 8px;\\n}\\n.text-sm {\\n font-size: 0.875rem;\\n line-height: 1.25rem;\\n}\\n.text-xs {\\n font-size: 0.75rem;\\n line-height: 1rem;\\n}\\n.font-bold {\\n font-weight: 700;\\n}\\n.uppercase {\\n text-transform: uppercase;\\n}\\n.capitalize {\\n text-transform: capitalize;\\n}\\n.italic {\\n font-style: italic;\\n}\\n.text-white {\\n --tw-text-opacity: 1;\\n color: rgb(255 255 255 / var(--tw-text-opacity, 1));\\n}\\n.opacity-50 {\\n opacity: 0.5;\\n}\\n.shadow {\\n --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);\\n --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);\\n box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);\\n}\\n.outline {\\n outline-style: solid;\\n}\\n.blur {\\n --tw-blur: blur(8px);\\n filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);\\n}\\n.grayscale {\\n --tw-grayscale: grayscale(100%);\\n filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);\\n}\\n.filter {\\n filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);\\n}\\n.backdrop-filter {\\n backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);\\n}\\n.transition {\\n transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;\\n transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\\n transition-duration: 150ms;\\n}\\n.ease-in {\\n transition-timing-function: cubic-bezier(0.4, 0, 1, 1);\\n}\\n.ease-in-out {\\n transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\\n}\\n.ease-out {\\n transition-timing-function: cubic-bezier(0, 0, 0.2, 1);\\n}\\n\""],"mappings":";AAAA,sBAAe"}
@@ -53,9 +53,10 @@ interface WaitForVideoContentResult {
53
53
  blankVideos: string[];
54
54
  }
55
55
  /**
56
- * Wait for video canvases within a timegroup to have content.
57
- * Only checks videos that should be visible at the current time.
58
- * Returns result with ready status and list of blank video names.
56
+ * Wait for media content (videos and images) within a timegroup to be ready.
57
+ * - ef-video: waits for the shadow canvas to have non-transparent pixels.
58
+ * - ef-image: waits for contentReadyState to reach "ready" or "error".
59
+ * Only checks elements that should be visible at the current time.
59
60
  */
60
61
  declare function waitForVideoContent(timegroup: EFTimegroup, timeMs: number, maxWaitMs: number): Promise<WaitForVideoContentResult>;
61
62
  /**
@@ -127,46 +127,44 @@ function canvasHasContent(canvas) {
127
127
  }
128
128
  }
129
129
  /**
130
- * Wait for video canvases within a timegroup to have content.
131
- * Only checks videos that should be visible at the current time.
132
- * Returns result with ready status and list of blank video names.
130
+ * Returns true if the element is visible at the given time and all its
131
+ * ancestor timegroups (up to but not including `timegroup`) are also visible.
132
+ */
133
+ function isVisibleInContext(element, timegroup, timeMs) {
134
+ if (!isVisibleAtTime(element, timeMs)) return false;
135
+ let parent = element.parentElement;
136
+ while (parent && parent !== timegroup) {
137
+ if (parent.tagName === "EF-TIMEGROUP" && !isVisibleAtTime(parent, timeMs)) return false;
138
+ parent = parent.parentElement;
139
+ }
140
+ return true;
141
+ }
142
+ /**
143
+ * Wait for media content (videos and images) within a timegroup to be ready.
144
+ * - ef-video: waits for the shadow canvas to have non-transparent pixels.
145
+ * - ef-image: waits for contentReadyState to reach "ready" or "error".
146
+ * Only checks elements that should be visible at the current time.
133
147
  */
134
148
  async function waitForVideoContent(timegroup, timeMs, maxWaitMs) {
135
149
  const startTime = performance.now();
136
- const allVideos = timegroup.querySelectorAll("ef-video");
137
- if (allVideos.length === 0) return {
150
+ const allVideos = Array.from(timegroup.querySelectorAll("ef-video")).filter((el) => isVisibleInContext(el, timegroup, timeMs));
151
+ const allImages = Array.from(timegroup.querySelectorAll("ef-image")).filter((el) => isVisibleInContext(el, timegroup, timeMs));
152
+ if (allVideos.length === 0 && allImages.length === 0) return {
138
153
  ready: true,
139
154
  blankVideos: []
140
155
  };
141
- const visibleVideos = Array.from(allVideos).filter((video) => {
142
- if (!isVisibleAtTime(video, timeMs)) return false;
143
- let parent = video.parentElement;
144
- while (parent && parent !== timegroup) {
145
- if (parent.tagName === "EF-TIMEGROUP" && !isVisibleAtTime(parent, timeMs)) return false;
146
- parent = parent.parentElement;
147
- }
148
- return true;
149
- });
150
- if (visibleVideos.length === 0) return {
151
- ready: true,
152
- blankVideos: []
153
- };
154
- const getBlankVideoNames = () => visibleVideos.filter((video) => {
156
+ const isVideoReady = (video) => {
155
157
  const shadowCanvas = video.shadowRoot?.querySelector("canvas");
156
- return shadowCanvas && !canvasHasContent(shadowCanvas);
157
- }).map((v) => v.src || v.id || "unnamed");
158
+ if (!shadowCanvas || shadowCanvas.width === 0 || shadowCanvas.height === 0) return true;
159
+ return canvasHasContent(shadowCanvas);
160
+ };
161
+ const isImageReady = (image) => {
162
+ const state = image.contentReadyState;
163
+ return state === "ready" || state === "error";
164
+ };
165
+ const getBlankNames = () => [...allVideos.filter((v) => !isVideoReady(v)).map((v) => v.src || v.id || "unnamed"), ...allImages.filter((i) => !isImageReady(i)).map((i) => i.src || i.id || "unnamed")];
158
166
  while (performance.now() - startTime < maxWaitMs) {
159
- let allHaveContent = true;
160
- for (const video of visibleVideos) {
161
- const shadowCanvas = video.shadowRoot?.querySelector("canvas");
162
- if (shadowCanvas && shadowCanvas.width > 0 && shadowCanvas.height > 0) {
163
- if (!canvasHasContent(shadowCanvas)) {
164
- allHaveContent = false;
165
- break;
166
- }
167
- }
168
- }
169
- if (allHaveContent) return {
167
+ if (allVideos.every(isVideoReady) && allImages.every(isImageReady)) return {
170
168
  ready: true,
171
169
  blankVideos: []
172
170
  };
@@ -174,7 +172,7 @@ async function waitForVideoContent(timegroup, timeMs, maxWaitMs) {
174
172
  }
175
173
  return {
176
174
  ready: false,
177
- blankVideos: getBlankVideoNames()
175
+ blankVideos: getBlankNames()
178
176
  };
179
177
  }
180
178
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"renderTimegroupToCanvas.js","names":["timeMs: number","timeoutMs: number","blankVideos: string[]","renderState: RenderState","options: CanvasPreviewOptions","pendingResolutionScale: number | null","captureCanvas: HTMLCanvasElement | null","captureCtx: HtmlInCanvasContext | null","originalParent: ParentNode | null","originalNextSibling: ChildNode | null"],"sources":["../../src/preview/renderTimegroupToCanvas.ts"],"sourcesContent":["import type { EFTimegroup } from \"../elements/EFTimegroup.js\";\nimport type {\n CaptureOptions,\n CaptureFromCloneOptions,\n GeneratedThumbnail,\n GenerateThumbnailsOptions,\n ThumbnailQueue,\n CanvasPreviewResult,\n CanvasPreviewOptions,\n} from \"./renderTimegroupToCanvas.types.js\";\nimport { RenderContext } from \"./RenderContext.js\";\nimport { FrameController } from \"./FrameController.js\";\nimport { captureTimelineToDataUri } from \"./rendering/serializeTimelineDirect.js\";\nimport { updateAnimations, type AnimatableElement } from \"../elements/updateAnimations.js\";\n\n// Re-export renderer types for external use\nexport type { RenderOptions, RenderResult, Renderer } from \"./renderers.js\";\nexport { getEffectiveRenderMode, isCanvas, isImage } from \"./renderers.js\";\nimport {\n type TemporalElement,\n isVisibleAtTime,\n DEFAULT_WIDTH,\n DEFAULT_HEIGHT,\n DEFAULT_CAPTURE_SCALE,\n DEFAULT_BLOCKING_TIMEOUT_MS,\n} from \"./previewTypes.js\";\nimport { defaultProfiler } from \"./RenderProfiler.js\";\nimport { logger } from \"./logger.js\";\n\n// Import rendering modules\nimport { loadImageFromDataUri } from \"./rendering/loadImage.js\";\nimport { createDprCanvas, renderToImageNative } from \"./rendering/renderToImageNative.js\";\nimport { clearInlineImageCache, getInlineImageCacheSize } from \"./rendering/inlineImages.js\";\nimport { isNativeCanvasApiAvailable, getRenderMode } from \"./previewSettings.js\";\nimport type { HtmlInCanvasContext, HtmlInCanvasElement } from \"./rendering/types.js\";\n\n// Re-export rendering types and functions for external use\nexport { loadImageFromDataUri };\n\n// ============================================================================\n// Constants (module-specific, not shared)\n// ============================================================================\n\n/** Number of rows to sample when checking canvas content */\nconst CANVAS_SAMPLE_STRIP_HEIGHT = 4;\n\n// ============================================================================\n// Types\n// ============================================================================\n\n// Re-export types from type-only module (zero side effects)\nexport type {\n ContentReadyMode,\n CaptureOptions,\n CaptureFromCloneOptions,\n GeneratedThumbnail,\n GenerateThumbnailsOptions,\n ThumbnailQueue,\n CanvasPreviewResult,\n CanvasPreviewOptions,\n} from \"./renderTimegroupToCanvas.types.js\";\n\n/**\n * Error thrown when video content is not ready within the blocking timeout.\n */\nexport class ContentNotReadyError extends Error {\n constructor(\n public readonly timeMs: number,\n public readonly timeoutMs: number,\n public readonly blankVideos: string[],\n ) {\n super(\n `Video content not ready at ${timeMs}ms after ${timeoutMs}ms timeout. Blank videos: ${blankVideos.join(\", \")}`,\n );\n this.name = \"ContentNotReadyError\";\n }\n}\n\n// ============================================================================\n// Module State (reset via resetRenderState)\n// ============================================================================\n\n/**\n * Module-level render state including caches and reusable objects.\n */\ninterface RenderState {\n inlineImageCache: Map<string, string>;\n layoutInitializedCanvases: WeakSet<HTMLCanvasElement>;\n xmlSerializer: XMLSerializer | null;\n textEncoder: TextEncoder;\n metrics: {\n inlineImageCacheHits: number;\n inlineImageCacheMisses: number;\n inlineImageCacheEvictions: number;\n };\n}\n\n/**\n * Module-level state for render operations.\n * Note: xmlSerializer is lazy-initialized for Node.js compatibility\n */\nconst renderState: RenderState = {\n inlineImageCache: new Map(),\n layoutInitializedCanvases: new WeakSet(),\n xmlSerializer: null, // Lazy-initialized in browser context\n textEncoder: new TextEncoder(),\n metrics: {\n inlineImageCacheHits: 0,\n inlineImageCacheMisses: 0,\n inlineImageCacheEvictions: 0,\n },\n};\n\n/**\n * Get the current render state for testing and debugging.\n * @returns The module-level render state object\n */\nexport function getRenderState(): RenderState {\n return renderState;\n}\n\n/**\n * Get cache metrics for monitoring performance.\n * @returns Object with cache hit/miss/eviction counts\n */\nexport function getCacheMetrics(): RenderState[\"metrics\"] {\n return { ...renderState.metrics };\n}\n\n/**\n * Reset cache metrics to zero.\n */\nexport function resetCacheMetrics(): void {\n renderState.metrics.inlineImageCacheHits = 0;\n renderState.metrics.inlineImageCacheMisses = 0;\n renderState.metrics.inlineImageCacheEvictions = 0;\n}\n\n/**\n * Reset all module state including profiling counters, caches, and logging flags.\n * Call at the start of export sessions to ensure clean state.\n */\nexport function resetRenderState(): void {\n defaultProfiler.reset();\n clearInlineImageCache();\n resetCacheMetrics();\n}\n\n// Re-export cache management functions\nexport { clearInlineImageCache, getInlineImageCacheSize };\n\n/**\n * DEBUG: Capture a single thumbnail at the current time.\n * Call from console: window.debugCaptureThumbnail()\n */\nif (typeof window !== \"undefined\") {\n (window as any).debugCaptureThumbnail = async function () {\n const timegroup = document.querySelector(\"ef-timegroup\") as any;\n if (!timegroup) {\n console.error(\"No timegroup found\");\n return;\n }\n\n const currentTime = timegroup.currentTimeMs ?? 0;\n\n try {\n const result = await captureTimegroupAtTime(timegroup, {\n timeMs: currentTime,\n scale: 0.25,\n contentReadyMode: \"blocking\",\n blockingTimeoutMs: 1000,\n });\n\n // Create a temporary img element to display the result\n const img = document.createElement(\"img\");\n if (result instanceof HTMLCanvasElement) {\n img.src = result.toDataURL();\n } else if (result instanceof HTMLImageElement) {\n img.src = result.src;\n }\n img.style.cssText = \"position:fixed;top:10px;right:10px;border:2px solid red;z-index:99999;\";\n document.body.appendChild(img);\n\n return result;\n } catch (err) {\n console.error(\"[DEBUG] Capture failed:\", err);\n throw err;\n }\n };\n}\n\n// ============================================================================\n// Internal Helpers\n// ============================================================================\n\n/**\n * Wait for next animation frame (allows browser to complete layout)\n */\nfunction waitForFrame(): Promise<void> {\n return new Promise((resolve) => requestAnimationFrame(() => resolve()));\n}\n\n/**\n * Check if a canvas has any rendered content (not all transparent/uninitialized).\n * Returns true if there's ANY non-transparent pixel.\n */\nfunction canvasHasContent(canvas: HTMLCanvasElement): boolean {\n const ctx = canvas.getContext(\"2d\", { willReadFrequently: true });\n if (!ctx) return false;\n\n try {\n const width = canvas.width;\n const height = canvas.height;\n if (width === 0 || height === 0) return false;\n\n // Sample a horizontal strip across the middle of the canvas\n // This catches most video content even if edges are black\n const stripY = Math.floor(height / 2);\n const imageData = ctx.getImageData(0, stripY, width, CANVAS_SAMPLE_STRIP_HEIGHT);\n const data = imageData.data;\n\n // Check if ANY pixel has non-zero alpha (is not transparent)\n // A truly blank/uninitialized canvas has all pixels at [0,0,0,0]\n // A black video frame would have pixels at [0,0,0,255] (opaque black)\n for (let i = 3; i < data.length; i += 4) {\n if (data[i] !== 0) {\n return true;\n }\n }\n\n return false;\n } catch {\n // Canvas might be tainted, assume it has content\n return true;\n }\n}\n\ninterface WaitForVideoContentResult {\n ready: boolean;\n blankVideos: string[];\n}\n\n/**\n * Wait for video canvases within a timegroup to have content.\n * Only checks videos that should be visible at the current time.\n * Returns result with ready status and list of blank video names.\n */\nexport async function waitForVideoContent(\n timegroup: EFTimegroup,\n timeMs: number,\n maxWaitMs: number,\n): Promise<WaitForVideoContentResult> {\n const startTime = performance.now();\n\n // Find all video elements in the timegroup (including nested)\n const allVideos = timegroup.querySelectorAll(\"ef-video\");\n if (allVideos.length === 0) return { ready: true, blankVideos: [] };\n\n // Filter to only videos that should be visible at this time\n const visibleVideos = Array.from(allVideos).filter((video) => {\n // Check if video itself is in time range\n if (!isVisibleAtTime(video, timeMs)) return false;\n\n // Check if all ancestor timegroups are in time range\n let parent = video.parentElement;\n while (parent && parent !== timegroup) {\n if (parent.tagName === \"EF-TIMEGROUP\" && !isVisibleAtTime(parent, timeMs)) {\n return false;\n }\n parent = parent.parentElement;\n }\n return true;\n });\n\n if (visibleVideos.length === 0) return { ready: true, blankVideos: [] };\n\n const getBlankVideoNames = () =>\n visibleVideos\n .filter((video) => {\n const shadowCanvas = video.shadowRoot?.querySelector(\"canvas\");\n return shadowCanvas && !canvasHasContent(shadowCanvas);\n })\n .map((v) => (v as TemporalElement).src || v.id || \"unnamed\");\n\n while (performance.now() - startTime < maxWaitMs) {\n let allHaveContent = true;\n\n for (const video of visibleVideos) {\n const shadowCanvas = video.shadowRoot?.querySelector(\"canvas\");\n if (shadowCanvas && shadowCanvas.width > 0 && shadowCanvas.height > 0) {\n if (!canvasHasContent(shadowCanvas)) {\n allHaveContent = false;\n break;\n }\n }\n }\n\n if (allHaveContent) return { ready: true, blankVideos: [] };\n\n // Wait a bit and check again\n await waitForFrame();\n }\n\n return { ready: false, blankVideos: getBlankVideoNames() };\n}\n\n/**\n * Captures a frame from an already-seeked render clone.\n * Used internally by captureBatch for efficiency (reuses one clone across all captures).\n *\n * @param renderClone - A render clone that has already been seeked to the target time\n * @param renderContainer - The container holding the render clone (from createRenderClone)\n * @param options - Capture options\n * @returns Canvas or Image with the rendered frame (both are CanvasImageSource)\n */\nexport async function captureFromClone(\n renderClone: EFTimegroup,\n _renderContainer: HTMLElement,\n options: CaptureFromCloneOptions = {},\n): Promise<CanvasImageSource> {\n const {\n scale = DEFAULT_CAPTURE_SCALE,\n contentReadyMode = \"immediate\",\n blockingTimeoutMs = DEFAULT_BLOCKING_TIMEOUT_MS,\n originalTimegroup,\n timeMs: explicitTimeMs,\n canvasMode,\n } = options;\n\n // Use explicit time if provided, otherwise fall back to clone's currentTimeMs\n // CRITICAL: Using explicit time ensures temporal visibility checks are accurate\n // NOTE: Must be defined BEFORE any logging that references timeMs\n const timeMs = explicitTimeMs ?? renderClone.currentTimeMs;\n\n // Use original timegroup dimensions if available, otherwise clone dimensions\n const sourceForDimensions = originalTimegroup ?? renderClone;\n const width = sourceForDimensions.offsetWidth || DEFAULT_WIDTH;\n const height = sourceForDimensions.offsetHeight || DEFAULT_HEIGHT;\n\n // NOTE: seekForRender() has already:\n // 1. Called frameController.renderFrame() to coordinate FrameRenderable elements\n // 2. Awaited #executeCustomFrameTasks() so frame tasks are complete\n // No need to call frameController.renderFrame() again - it would fire tasks redundantly\n\n if (contentReadyMode === \"blocking\") {\n const result = await waitForVideoContent(renderClone, timeMs, blockingTimeoutMs);\n if (!result.ready) {\n throw new ContentNotReadyError(timeMs, blockingTimeoutMs, result.blankVideos);\n }\n }\n\n // Determine effective canvas mode:\n // 1. If explicitly specified, use that\n // 2. If \"native\" is requested but not available, fall back to foreignObject\n // 3. If not specified, default to foreignObject for compatibility\n const effectiveCanvasMode = (() => {\n if (!canvasMode) return \"foreignObject\";\n if (canvasMode === \"native\" && !isNativeCanvasApiAvailable()) {\n logger.debug(\n \"[captureFromClone] Native canvas mode requested but not available, falling back to foreignObject\",\n );\n return \"foreignObject\";\n }\n return canvasMode;\n })();\n\n // Create RenderContext for caching during this capture operation (only needed for foreignObject)\n const renderContext = new RenderContext();\n\n try {\n if (effectiveCanvasMode === \"native\") {\n // NATIVE PATH: Use drawElementImage API (~1.76x faster than foreignObject)\n // No DOM serialization, no canvas-to-dataURL encoding, no image loading\n // Direct browser-native rendering\n\n const t0 = performance.now();\n const canvas = await renderToImageNative(renderClone, width, height, {\n skipDprScaling: true, // Use 1x DPR for video export (4x fewer pixels!)\n });\n const renderTime = performance.now() - t0;\n\n logger.debug(\n `[captureFromClone] native render=${renderTime.toFixed(0)}ms (canvasScale=${scale})`,\n );\n\n return canvas;\n } else {\n // FOREIGNOBJECT PATH: Serialize DOM → SVG → Image → Canvas\n // More compatible but slower than native path\n\n // NOTE: seekForRender() has already ensured rendering is complete, including:\n // - Lit updates propagated\n // - All LitElement descendants updated\n // - frameController.renderFrame() called for FrameRenderable elements\n // - Layout stabilization complete\n // No additional RAF wait needed - can serialize immediately\n\n const t0 = performance.now();\n const dataUri = await captureTimelineToDataUri(renderClone, width, height, {\n renderContext,\n canvasScale: scale,\n timeMs,\n });\n const serializeTime = performance.now() - t0;\n\n const t1 = performance.now();\n const image = await loadImageFromDataUri(dataUri);\n const loadTime = performance.now() - t1;\n\n logger.debug(\n `[captureFromClone] foreignObject serialize=${serializeTime.toFixed(0)}ms, load=${loadTime.toFixed(0)}ms (canvasScale=${scale})`,\n );\n\n // Return image directly - no copy needed!\n return image;\n }\n } finally {\n // Ensure RenderContext is disposed even if an error occurs\n renderContext.dispose();\n }\n}\n\n/**\n * Captures a single frame from a timegroup at a specific time.\n *\n * CLONE-TIMELINE ARCHITECTURE:\n * Creates an independent render clone, seeks it to the target time, and captures.\n * Prime-timeline is NEVER seeked - user can continue previewing/editing during capture.\n *\n * @param timegroup - The source timegroup\n * @param options - Capture options including timeMs, scale, contentReadyMode\n * @returns Canvas with the rendered frame\n * @throws ContentNotReadyError if blocking mode times out waiting for video content\n */\nexport async function captureTimegroupAtTime(\n timegroup: EFTimegroup,\n options: CaptureOptions,\n): Promise<CanvasImageSource> {\n const {\n timeMs,\n scale = DEFAULT_CAPTURE_SCALE,\n // skipRestore is deprecated with Clone-timeline (Prime is never seeked)\n contentReadyMode = \"immediate\",\n blockingTimeoutMs = DEFAULT_BLOCKING_TIMEOUT_MS,\n canvasMode,\n skipClone = false,\n } = options;\n\n if (skipClone) {\n // DIRECT RENDERING: Skip clone creation for headless server rendering\n // Seek prime timeline directly and capture from it\n // WARNING: This modifies the prime timeline! Only use in headless contexts.\n\n const seekStart = performance.now();\n await timegroup.seekForRender(timeMs);\n const seekMs = performance.now() - seekStart;\n\n const renderStart = performance.now();\n // Use timegroup's actual container (parentElement or document.body as fallback)\n const container = (timegroup.parentElement || document.body) as HTMLElement;\n const result = await captureFromClone(timegroup, container, {\n scale,\n contentReadyMode,\n blockingTimeoutMs,\n originalTimegroup: undefined, // No original since we're rendering the prime\n canvasMode,\n timeMs, // Pass explicit time since we're not using a clone\n });\n const renderMs = performance.now() - renderStart;\n\n // Store timing (no clone time since we skipped it)\n if (typeof result === \"object\" && result !== null) {\n (result as any).__perfTiming = { cloneMs: 0, seekMs, renderMs };\n }\n\n return result;\n }\n\n // CLONE-TIMELINE: Create a short-lived render clone for this capture\n // Prime-timeline is NEVER seeked - clone is fully independent\n const cloneStart = performance.now();\n const {\n clone: renderClone,\n container: renderContainer,\n cleanup: cleanupRenderClone,\n } = await timegroup.createRenderClone();\n const cloneMs = performance.now() - cloneStart;\n\n try {\n // Seek the clone to target time (Prime stays at user position)\n // Use seekForRender which bypasses duration clamping - render clones may have\n // zero duration initially until media durations are computed, but we still\n // want to seek to the requested time for capture purposes.\n const seekStart = performance.now();\n await renderClone.seekForRender(timeMs);\n const seekMs = performance.now() - seekStart;\n\n // Use the shared capture helper\n const renderStart = performance.now();\n const result = await captureFromClone(renderClone, renderContainer, {\n scale,\n contentReadyMode,\n blockingTimeoutMs,\n originalTimegroup: timegroup,\n canvasMode,\n });\n const renderMs = performance.now() - renderStart;\n\n // Store timing on the result for access by callers (if they need it)\n // Note: CanvasImageSource doesn't support custom properties, but we can attach them anyway\n if (typeof result === \"object\" && result !== null) {\n (result as any).__perfTiming = { cloneMs, seekMs, renderMs };\n }\n\n return result;\n } finally {\n // Clean up the render clone\n cleanupRenderClone();\n }\n}\n\n/**\n * Generate thumbnails using an existing render clone and mutable queue.\n * The queue can be modified while generation is in progress.\n *\n * @param renderClone - Pre-created render clone to use\n * @param renderContainer - Container for the render clone\n * @param queue - Mutable queue that provides timestamps\n * @param options - Capture options (scale, contentReadyMode, etc.)\n * @yields Objects with { timeMs, canvas } for each captured thumbnail\n *\n * @example\n * ```ts\n * const queue = new MutableTimestampQueue();\n * queue.reset([0, 100, 200]);\n *\n * for await (const { timeMs, canvas } of generateThumbnailsFromClone(clone, container, queue)) {\n * cache.set(timeMs, canvas);\n * // Queue can be modified here while generator continues\n * }\n * ```\n */\nexport async function* generateThumbnailsFromClone(\n renderClone: EFTimegroup,\n renderContainer: HTMLElement,\n queue: ThumbnailQueue,\n options: GenerateThumbnailsOptions = {},\n): AsyncGenerator<GeneratedThumbnail> {\n const {\n scale = DEFAULT_CAPTURE_SCALE,\n contentReadyMode = \"immediate\",\n blockingTimeoutMs = DEFAULT_BLOCKING_TIMEOUT_MS,\n signal,\n } = options;\n\n while (true) {\n // Check if aborted before starting work\n if (signal?.aborted) {\n break;\n }\n\n const timeMs = queue.shift();\n if (timeMs === undefined) {\n // Queue is empty, generator exits\n break;\n }\n\n // Seek the clone to the target time\n await renderClone.seekForRender(timeMs);\n\n // Check if aborted after seek (before expensive capture)\n if (signal?.aborted) {\n break;\n }\n\n // Capture from the seeked clone, passing explicit timeMs\n const canvas = await captureFromClone(renderClone, renderContainer, {\n scale,\n contentReadyMode,\n blockingTimeoutMs,\n timeMs, // CRITICAL: Pass explicit time for accurate temporal visibility\n });\n\n // Yield the result with explicit timestamp association\n yield { timeMs, canvas };\n }\n}\n\n/**\n * Generate thumbnails for multiple timestamps efficiently using a single render clone.\n * This avoids the overhead of creating/destroying a clone for each thumbnail.\n *\n * @param timegroup - The timegroup to capture\n * @param timestamps - Array of timestamps to capture (in milliseconds)\n * @param options - Capture options (scale, contentReadyMode, etc.)\n * @param signal - Optional AbortSignal to cancel generation\n * @yields Objects with { timeMs, canvas } for each captured thumbnail\n *\n * @example\n * ```ts\n * for await (const { timeMs, canvas } of generateThumbnails(tg, [0, 100, 200])) {\n * console.log(`Got thumbnail for ${timeMs}ms`);\n * thumbnailCache.set(timeMs, canvas);\n * }\n * ```\n */\nexport async function* generateThumbnails(\n timegroup: EFTimegroup,\n timestamps: number[],\n options: GenerateThumbnailsOptions = {},\n signal?: AbortSignal,\n): AsyncGenerator<GeneratedThumbnail> {\n const {\n scale = DEFAULT_CAPTURE_SCALE,\n contentReadyMode = \"immediate\",\n blockingTimeoutMs = DEFAULT_BLOCKING_TIMEOUT_MS,\n } = options;\n\n // Create a single render clone for all thumbnails\n const {\n clone: renderClone,\n container: renderContainer,\n cleanup: cleanupRenderClone,\n } = await timegroup.createRenderClone();\n\n try {\n for (const timeMs of timestamps) {\n // Check for abort before each capture\n signal?.throwIfAborted();\n\n // Seek the clone to the target time\n await renderClone.seekForRender(timeMs);\n\n // Capture from the seeked clone\n const canvas = await captureFromClone(renderClone, renderContainer, {\n scale,\n contentReadyMode,\n blockingTimeoutMs,\n originalTimegroup: timegroup,\n });\n\n // Yield the result with explicit timestamp association\n yield { timeMs, canvas };\n }\n } finally {\n // Always clean up the render clone\n cleanupRenderClone();\n }\n}\n\n/** Epsilon for comparing time values (ms) - times within this are considered equal */\nconst TIME_EPSILON_MS = 1;\n\n/** Default scale for preview rendering */\nconst DEFAULT_PREVIEW_SCALE = 1;\n\n/** Default resolution scale (full resolution) */\nconst DEFAULT_RESOLUTION_SCALE = 1;\n\n/**\n * Convert relative time to absolute time for a timegroup.\n * Nested timegroup children have ABSOLUTE startTimeMs values,\n * so relative capture times must be converted for temporal culling.\n */\nfunction toAbsoluteTime(timegroup: EFTimegroup, relativeTimeMs: number): number {\n return relativeTimeMs + (timegroup.startTimeMs ?? 0);\n}\n\n/**\n * Renders a timegroup preview to a canvas using SVG foreignObject.\n *\n * Captures the prime timeline's current visual state including DOM changes\n * from frame tasks (SVG paths, canvas content, text updates, etc.).\n *\n * Optimized with:\n * - Passive clone structure rebuilt each frame from prime's current state\n * - Temporal bucketing for time-based culling\n * - RenderContext for canvas pixel caching across frames\n * - Resolution scaling for performance (renders at lower resolution, CSS upscales)\n *\n * @param timegroup - The source timegroup to preview (prime timeline)\n * @param scaleOrOptions - Scale factor (default 1) or options object\n * @returns Object with canvas and refresh function\n */\nexport function renderTimegroupToCanvas(\n timegroup: EFTimegroup,\n scaleOrOptions: number | CanvasPreviewOptions = DEFAULT_PREVIEW_SCALE,\n): CanvasPreviewResult {\n // Normalize options\n const options: CanvasPreviewOptions =\n typeof scaleOrOptions === \"number\" ? { scale: scaleOrOptions } : scaleOrOptions;\n\n const scale = options.scale ?? DEFAULT_PREVIEW_SCALE;\n // These are mutable to support dynamic resolution changes\n let currentResolutionScale = options.resolutionScale ?? DEFAULT_RESOLUTION_SCALE;\n\n const width = timegroup.offsetWidth || DEFAULT_WIDTH;\n const height = timegroup.offsetHeight || DEFAULT_HEIGHT;\n const dpr = (typeof window !== \"undefined\" ? window.devicePixelRatio : 1) || 1;\n\n // Calculate effective render dimensions (internal resolution) - mutable\n let renderWidth = Math.floor(width * currentResolutionScale);\n let renderHeight = Math.floor(height * currentResolutionScale);\n\n // Create canvas with proper DPR handling\n const canvas = createDprCanvas({\n renderWidth,\n renderHeight,\n scale,\n fullWidth: width,\n fullHeight: height,\n dpr,\n });\n\n // Return canvas directly - no wrapper needed\n const wrapperContainer = canvas;\n\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) {\n throw new Error(\"Failed to get canvas 2d context\");\n }\n\n // Track render state\n let rendering = false;\n let lastTimeMs = -1;\n let disposed = false;\n\n // Invalidate lastTimeMs when composition structure or attributes change so\n // refresh() re-renders even when currentTimeMs hasn't changed (e.g. paused edits).\n const compositionObserver = new MutationObserver(() => {\n if (!rendering) lastTimeMs = -1;\n });\n compositionObserver.observe(timegroup, {\n attributes: true,\n childList: true,\n subtree: true,\n });\n\n // Create RenderContext for caching across refresh calls (foreignObject only)\n const renderContext = new RenderContext();\n\n // Create FrameController for coordinating element rendering\n // Cached for the lifetime of this preview instance\n const frameController = new FrameController(timegroup);\n\n // Log resolution scale on first render for debugging\n let hasLoggedScale = false;\n\n // Pending resolution change - applied at start of next refresh to avoid blanking\n let pendingResolutionScale: number | null = null;\n\n // Use the user's render mode preference. Native requires the timegroup to be\n // inside a <canvas layoutsubtree> for drawElementImage to work.\n const useNative = getRenderMode() === \"native\" && isNativeCanvasApiAvailable();\n let captureCanvas: HTMLCanvasElement | null = null;\n let captureCtx: HtmlInCanvasContext | null = null;\n let originalParent: ParentNode | null = null;\n let originalNextSibling: ChildNode | null = null;\n let savedClipPath = \"\";\n let savedPointerEvents = \"\";\n\n if (useNative) {\n captureCanvas = document.createElement(\"canvas\");\n captureCanvas.setAttribute(\"layoutsubtree\", \"\");\n (captureCanvas as HtmlInCanvasElement).layoutSubtree = true;\n captureCanvas.width = renderWidth;\n captureCanvas.height = renderHeight;\n captureCanvas.style.cssText = `position:fixed;left:0;top:0;width:${width}px;height:${height}px;opacity:0;pointer-events:none;z-index:-9999;`;\n originalParent = timegroup.parentNode;\n originalNextSibling = timegroup.nextSibling;\n savedClipPath = timegroup.style.clipPath;\n savedPointerEvents = timegroup.style.pointerEvents;\n timegroup.style.clipPath = \"\";\n timegroup.style.pointerEvents = \"\";\n captureCanvas.appendChild(timegroup);\n document.body.appendChild(captureCanvas);\n captureCtx = captureCanvas.getContext(\"2d\") as HtmlInCanvasContext;\n void captureCanvas.offsetHeight;\n void timegroup.offsetHeight;\n }\n\n /**\n * Apply pending resolution scale changes.\n * Called at the start of refresh() before rendering, so the old content\n * stays visible until new content is ready to be drawn.\n */\n const applyPendingResolutionChange = (): void => {\n if (pendingResolutionScale === null) return;\n\n const newScale = pendingResolutionScale;\n pendingResolutionScale = null;\n\n currentResolutionScale = newScale;\n renderWidth = Math.floor(width * currentResolutionScale);\n renderHeight = Math.floor(height * currentResolutionScale);\n\n if (captureCanvas) {\n captureCanvas.width = renderWidth;\n captureCanvas.height = renderHeight;\n }\n };\n\n /**\n * Dynamically change resolution scale without rebuilding clone structure.\n * The actual change is deferred until next refresh() to avoid blanking -\n * old content stays visible until new content is ready.\n */\n const setResolutionScale = (newScale: number): void => {\n // Clamp to valid range\n newScale = Math.max(0.1, Math.min(1, newScale));\n\n if (newScale === currentResolutionScale && pendingResolutionScale === null) return;\n\n // Queue the change - will be applied at start of next refresh\n pendingResolutionScale = newScale;\n\n // Force re-render on next refresh by invalidating lastTimeMs\n lastTimeMs = -1;\n };\n\n const getResolutionScale = (): number => pendingResolutionScale ?? currentResolutionScale;\n\n const refresh = async (): Promise<void> => {\n if (disposed) return;\n\n const sourceTimeMs = timegroup.currentTimeMs ?? 0;\n const userTimeMs = timegroup.userTimeMs ?? 0;\n\n if (Math.abs(sourceTimeMs - userTimeMs) > TIME_EPSILON_MS) return;\n if (userTimeMs === lastTimeMs) return;\n if (rendering) return;\n\n lastTimeMs = userTimeMs;\n rendering = true;\n\n applyPendingResolutionChange();\n\n if (!hasLoggedScale) {\n hasLoggedScale = true;\n const mode = useNative ? \"native\" : \"foreignObject\";\n logger.debug(\n `[renderTimegroupToCanvas] Resolution scale: ${currentResolutionScale} (${width}x${height} → ${renderWidth}x${renderHeight}), canvas buffer: ${canvas.width}x${canvas.height}, CSS size: ${canvas.style.width}x${canvas.style.height}, renderMode: ${mode}`,\n );\n }\n\n try {\n await frameController.renderFrame(userTimeMs, {\n waitForLitUpdate: false,\n onAnimationsUpdate: (root) => {\n updateAnimations(root as AnimatableElement);\n },\n });\n\n if (useNative && captureCanvas && captureCtx) {\n if (captureCanvas.width !== width || captureCanvas.height !== height) {\n captureCtx.save();\n captureCtx.scale(captureCanvas.width / width, captureCanvas.height / height);\n captureCtx.drawElementImage(timegroup, 0, 0);\n captureCtx.restore();\n } else {\n captureCtx.drawElementImage(timegroup, 0, 0);\n }\n const targetWidth = Math.floor(renderWidth * scale * dpr);\n const targetHeight = Math.floor(renderHeight * scale * dpr);\n if (canvas.width !== targetWidth || canvas.height !== targetHeight) {\n canvas.width = targetWidth;\n canvas.height = targetHeight;\n } else {\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n }\n ctx.drawImage(captureCanvas, 0, 0, canvas.width, canvas.height);\n\n defaultProfiler.incrementRenderCount();\n } else {\n const absoluteTimeMs = toAbsoluteTime(timegroup, userTimeMs);\n\n const dataUri = await captureTimelineToDataUri(timegroup, width, height, {\n renderContext,\n canvasScale: currentResolutionScale,\n timeMs: absoluteTimeMs,\n });\n const image = await loadImageFromDataUri(dataUri);\n\n const targetWidth = Math.floor(renderWidth * scale * dpr);\n const targetHeight = Math.floor(renderHeight * scale * dpr);\n if (canvas.width !== targetWidth || canvas.height !== targetHeight) {\n canvas.width = targetWidth;\n canvas.height = targetHeight;\n } else {\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n }\n\n ctx.save();\n ctx.scale(dpr * scale, dpr * scale);\n ctx.drawImage(image, 0, 0, renderWidth, renderHeight);\n ctx.restore();\n\n defaultProfiler.incrementRenderCount();\n }\n } catch (e) {\n logger.error(\"Canvas preview render failed:\", e);\n } finally {\n rendering = false;\n }\n };\n\n /**\n * Dispose the preview and release resources.\n */\n const dispose = (): void => {\n if (disposed) return;\n disposed = true;\n compositionObserver.disconnect();\n frameController.abort();\n renderContext.dispose();\n\n // Restore timegroup to original DOM position if native mode moved it\n if (useNative && originalParent) {\n timegroup.style.clipPath = savedClipPath;\n timegroup.style.pointerEvents = savedPointerEvents;\n if (originalNextSibling) {\n originalParent.insertBefore(timegroup, originalNextSibling);\n } else {\n originalParent.appendChild(timegroup);\n }\n captureCanvas?.remove();\n }\n };\n\n // Do initial render\n refresh();\n\n return {\n container: wrapperContainer,\n canvas,\n refresh,\n setResolutionScale,\n getResolutionScale,\n dispose,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"renderTimegroupToCanvas.js","names":["timeMs: number","timeoutMs: number","blankVideos: string[]","renderState: RenderState","options: CanvasPreviewOptions","pendingResolutionScale: number | null","captureCanvas: HTMLCanvasElement | null","captureCtx: HtmlInCanvasContext | null","originalParent: ParentNode | null","originalNextSibling: ChildNode | null"],"sources":["../../src/preview/renderTimegroupToCanvas.ts"],"sourcesContent":["import type { EFTimegroup } from \"../elements/EFTimegroup.js\";\nimport type {\n CaptureOptions,\n CaptureFromCloneOptions,\n GeneratedThumbnail,\n GenerateThumbnailsOptions,\n ThumbnailQueue,\n CanvasPreviewResult,\n CanvasPreviewOptions,\n} from \"./renderTimegroupToCanvas.types.js\";\nimport { RenderContext } from \"./RenderContext.js\";\nimport { FrameController } from \"./FrameController.js\";\nimport { captureTimelineToDataUri } from \"./rendering/serializeTimelineDirect.js\";\nimport { updateAnimations, type AnimatableElement } from \"../elements/updateAnimations.js\";\n\n// Re-export renderer types for external use\nexport type { RenderOptions, RenderResult, Renderer } from \"./renderers.js\";\nexport { getEffectiveRenderMode, isCanvas, isImage } from \"./renderers.js\";\nimport {\n isVisibleAtTime,\n DEFAULT_WIDTH,\n DEFAULT_HEIGHT,\n DEFAULT_CAPTURE_SCALE,\n DEFAULT_BLOCKING_TIMEOUT_MS,\n} from \"./previewTypes.js\";\nimport { defaultProfiler } from \"./RenderProfiler.js\";\nimport { logger } from \"./logger.js\";\n\n// Import rendering modules\nimport { loadImageFromDataUri } from \"./rendering/loadImage.js\";\nimport { createDprCanvas, renderToImageNative } from \"./rendering/renderToImageNative.js\";\nimport { clearInlineImageCache, getInlineImageCacheSize } from \"./rendering/inlineImages.js\";\nimport { isNativeCanvasApiAvailable, getRenderMode } from \"./previewSettings.js\";\nimport type { HtmlInCanvasContext, HtmlInCanvasElement } from \"./rendering/types.js\";\n\n// Re-export rendering types and functions for external use\nexport { loadImageFromDataUri };\n\n// ============================================================================\n// Constants (module-specific, not shared)\n// ============================================================================\n\n/** Number of rows to sample when checking canvas content */\nconst CANVAS_SAMPLE_STRIP_HEIGHT = 4;\n\n// ============================================================================\n// Types\n// ============================================================================\n\n// Re-export types from type-only module (zero side effects)\nexport type {\n ContentReadyMode,\n CaptureOptions,\n CaptureFromCloneOptions,\n GeneratedThumbnail,\n GenerateThumbnailsOptions,\n ThumbnailQueue,\n CanvasPreviewResult,\n CanvasPreviewOptions,\n} from \"./renderTimegroupToCanvas.types.js\";\n\n/**\n * Error thrown when video content is not ready within the blocking timeout.\n */\nexport class ContentNotReadyError extends Error {\n constructor(\n public readonly timeMs: number,\n public readonly timeoutMs: number,\n public readonly blankVideos: string[],\n ) {\n super(\n `Video content not ready at ${timeMs}ms after ${timeoutMs}ms timeout. Blank videos: ${blankVideos.join(\", \")}`,\n );\n this.name = \"ContentNotReadyError\";\n }\n}\n\n// ============================================================================\n// Module State (reset via resetRenderState)\n// ============================================================================\n\n/**\n * Module-level render state including caches and reusable objects.\n */\ninterface RenderState {\n inlineImageCache: Map<string, string>;\n layoutInitializedCanvases: WeakSet<HTMLCanvasElement>;\n xmlSerializer: XMLSerializer | null;\n textEncoder: TextEncoder;\n metrics: {\n inlineImageCacheHits: number;\n inlineImageCacheMisses: number;\n inlineImageCacheEvictions: number;\n };\n}\n\n/**\n * Module-level state for render operations.\n * Note: xmlSerializer is lazy-initialized for Node.js compatibility\n */\nconst renderState: RenderState = {\n inlineImageCache: new Map(),\n layoutInitializedCanvases: new WeakSet(),\n xmlSerializer: null, // Lazy-initialized in browser context\n textEncoder: new TextEncoder(),\n metrics: {\n inlineImageCacheHits: 0,\n inlineImageCacheMisses: 0,\n inlineImageCacheEvictions: 0,\n },\n};\n\n/**\n * Get the current render state for testing and debugging.\n * @returns The module-level render state object\n */\nexport function getRenderState(): RenderState {\n return renderState;\n}\n\n/**\n * Get cache metrics for monitoring performance.\n * @returns Object with cache hit/miss/eviction counts\n */\nexport function getCacheMetrics(): RenderState[\"metrics\"] {\n return { ...renderState.metrics };\n}\n\n/**\n * Reset cache metrics to zero.\n */\nexport function resetCacheMetrics(): void {\n renderState.metrics.inlineImageCacheHits = 0;\n renderState.metrics.inlineImageCacheMisses = 0;\n renderState.metrics.inlineImageCacheEvictions = 0;\n}\n\n/**\n * Reset all module state including profiling counters, caches, and logging flags.\n * Call at the start of export sessions to ensure clean state.\n */\nexport function resetRenderState(): void {\n defaultProfiler.reset();\n clearInlineImageCache();\n resetCacheMetrics();\n}\n\n// Re-export cache management functions\nexport { clearInlineImageCache, getInlineImageCacheSize };\n\n/**\n * DEBUG: Capture a single thumbnail at the current time.\n * Call from console: window.debugCaptureThumbnail()\n */\nif (typeof window !== \"undefined\") {\n (window as any).debugCaptureThumbnail = async function () {\n const timegroup = document.querySelector(\"ef-timegroup\") as any;\n if (!timegroup) {\n console.error(\"No timegroup found\");\n return;\n }\n\n const currentTime = timegroup.currentTimeMs ?? 0;\n\n try {\n const result = await captureTimegroupAtTime(timegroup, {\n timeMs: currentTime,\n scale: 0.25,\n contentReadyMode: \"blocking\",\n blockingTimeoutMs: 1000,\n });\n\n // Create a temporary img element to display the result\n const img = document.createElement(\"img\");\n if (result instanceof HTMLCanvasElement) {\n img.src = result.toDataURL();\n } else if (result instanceof HTMLImageElement) {\n img.src = result.src;\n }\n img.style.cssText = \"position:fixed;top:10px;right:10px;border:2px solid red;z-index:99999;\";\n document.body.appendChild(img);\n\n return result;\n } catch (err) {\n console.error(\"[DEBUG] Capture failed:\", err);\n throw err;\n }\n };\n}\n\n// ============================================================================\n// Internal Helpers\n// ============================================================================\n\n/**\n * Wait for next animation frame (allows browser to complete layout)\n */\nfunction waitForFrame(): Promise<void> {\n return new Promise((resolve) => requestAnimationFrame(() => resolve()));\n}\n\n/**\n * Check if a canvas has any rendered content (not all transparent/uninitialized).\n * Returns true if there's ANY non-transparent pixel.\n */\nfunction canvasHasContent(canvas: HTMLCanvasElement): boolean {\n const ctx = canvas.getContext(\"2d\", { willReadFrequently: true });\n if (!ctx) return false;\n\n try {\n const width = canvas.width;\n const height = canvas.height;\n if (width === 0 || height === 0) return false;\n\n // Sample a horizontal strip across the middle of the canvas\n // This catches most video content even if edges are black\n const stripY = Math.floor(height / 2);\n const imageData = ctx.getImageData(0, stripY, width, CANVAS_SAMPLE_STRIP_HEIGHT);\n const data = imageData.data;\n\n // Check if ANY pixel has non-zero alpha (is not transparent)\n // A truly blank/uninitialized canvas has all pixels at [0,0,0,0]\n // A black video frame would have pixels at [0,0,0,255] (opaque black)\n for (let i = 3; i < data.length; i += 4) {\n if (data[i] !== 0) {\n return true;\n }\n }\n\n return false;\n } catch {\n // Canvas might be tainted, assume it has content\n return true;\n }\n}\n\ninterface WaitForVideoContentResult {\n ready: boolean;\n blankVideos: string[];\n}\n\n/**\n * Returns true if the element is visible at the given time and all its\n * ancestor timegroups (up to but not including `timegroup`) are also visible.\n */\nfunction isVisibleInContext(element: Element, timegroup: EFTimegroup, timeMs: number): boolean {\n if (!isVisibleAtTime(element, timeMs)) return false;\n let parent = element.parentElement;\n while (parent && parent !== timegroup) {\n if (parent.tagName === \"EF-TIMEGROUP\" && !isVisibleAtTime(parent, timeMs)) return false;\n parent = parent.parentElement;\n }\n return true;\n}\n\n/**\n * Wait for media content (videos and images) within a timegroup to be ready.\n * - ef-video: waits for the shadow canvas to have non-transparent pixels.\n * - ef-image: waits for contentReadyState to reach \"ready\" or \"error\".\n * Only checks elements that should be visible at the current time.\n */\nexport async function waitForVideoContent(\n timegroup: EFTimegroup,\n timeMs: number,\n maxWaitMs: number,\n): Promise<WaitForVideoContentResult> {\n const startTime = performance.now();\n\n // Collect all media elements that need to be ready\n const allVideos = Array.from(timegroup.querySelectorAll(\"ef-video\")).filter((el) =>\n isVisibleInContext(el, timegroup, timeMs),\n );\n const allImages = Array.from(timegroup.querySelectorAll(\"ef-image\")).filter((el) =>\n isVisibleInContext(el, timegroup, timeMs),\n );\n\n if (allVideos.length === 0 && allImages.length === 0) return { ready: true, blankVideos: [] };\n\n const isVideoReady = (video: Element): boolean => {\n const shadowCanvas = video.shadowRoot?.querySelector(\"canvas\");\n if (!shadowCanvas || shadowCanvas.width === 0 || shadowCanvas.height === 0) return true;\n return canvasHasContent(shadowCanvas);\n };\n\n const isImageReady = (image: Element): boolean => {\n const state = (image as any).contentReadyState as string | undefined;\n return state === \"ready\" || state === \"error\";\n };\n\n const getBlankNames = () => [\n ...allVideos.filter((v) => !isVideoReady(v)).map((v) => (v as any).src || v.id || \"unnamed\"),\n ...allImages.filter((i) => !isImageReady(i)).map((i) => (i as any).src || i.id || \"unnamed\"),\n ];\n\n while (performance.now() - startTime < maxWaitMs) {\n if (allVideos.every(isVideoReady) && allImages.every(isImageReady)) {\n return { ready: true, blankVideos: [] };\n }\n await waitForFrame();\n }\n\n return { ready: false, blankVideos: getBlankNames() };\n}\n\n/**\n * Captures a frame from an already-seeked render clone.\n * Used internally by captureBatch for efficiency (reuses one clone across all captures).\n *\n * @param renderClone - A render clone that has already been seeked to the target time\n * @param renderContainer - The container holding the render clone (from createRenderClone)\n * @param options - Capture options\n * @returns Canvas or Image with the rendered frame (both are CanvasImageSource)\n */\nexport async function captureFromClone(\n renderClone: EFTimegroup,\n _renderContainer: HTMLElement,\n options: CaptureFromCloneOptions = {},\n): Promise<CanvasImageSource> {\n const {\n scale = DEFAULT_CAPTURE_SCALE,\n contentReadyMode = \"immediate\",\n blockingTimeoutMs = DEFAULT_BLOCKING_TIMEOUT_MS,\n originalTimegroup,\n timeMs: explicitTimeMs,\n canvasMode,\n } = options;\n\n // Use explicit time if provided, otherwise fall back to clone's currentTimeMs\n // CRITICAL: Using explicit time ensures temporal visibility checks are accurate\n // NOTE: Must be defined BEFORE any logging that references timeMs\n const timeMs = explicitTimeMs ?? renderClone.currentTimeMs;\n\n // Use original timegroup dimensions if available, otherwise clone dimensions\n const sourceForDimensions = originalTimegroup ?? renderClone;\n const width = sourceForDimensions.offsetWidth || DEFAULT_WIDTH;\n const height = sourceForDimensions.offsetHeight || DEFAULT_HEIGHT;\n\n // NOTE: seekForRender() has already:\n // 1. Called frameController.renderFrame() to coordinate FrameRenderable elements\n // 2. Awaited #executeCustomFrameTasks() so frame tasks are complete\n // No need to call frameController.renderFrame() again - it would fire tasks redundantly\n\n if (contentReadyMode === \"blocking\") {\n const result = await waitForVideoContent(renderClone, timeMs, blockingTimeoutMs);\n if (!result.ready) {\n throw new ContentNotReadyError(timeMs, blockingTimeoutMs, result.blankVideos);\n }\n }\n\n // Determine effective canvas mode:\n // 1. If explicitly specified, use that\n // 2. If \"native\" is requested but not available, fall back to foreignObject\n // 3. If not specified, default to foreignObject for compatibility\n const effectiveCanvasMode = (() => {\n if (!canvasMode) return \"foreignObject\";\n if (canvasMode === \"native\" && !isNativeCanvasApiAvailable()) {\n logger.debug(\n \"[captureFromClone] Native canvas mode requested but not available, falling back to foreignObject\",\n );\n return \"foreignObject\";\n }\n return canvasMode;\n })();\n\n // Create RenderContext for caching during this capture operation (only needed for foreignObject)\n const renderContext = new RenderContext();\n\n try {\n if (effectiveCanvasMode === \"native\") {\n // NATIVE PATH: Use drawElementImage API (~1.76x faster than foreignObject)\n // No DOM serialization, no canvas-to-dataURL encoding, no image loading\n // Direct browser-native rendering\n\n const t0 = performance.now();\n const canvas = await renderToImageNative(renderClone, width, height, {\n skipDprScaling: true, // Use 1x DPR for video export (4x fewer pixels!)\n });\n const renderTime = performance.now() - t0;\n\n logger.debug(\n `[captureFromClone] native render=${renderTime.toFixed(0)}ms (canvasScale=${scale})`,\n );\n\n return canvas;\n } else {\n // FOREIGNOBJECT PATH: Serialize DOM → SVG → Image → Canvas\n // More compatible but slower than native path\n\n // NOTE: seekForRender() has already ensured rendering is complete, including:\n // - Lit updates propagated\n // - All LitElement descendants updated\n // - frameController.renderFrame() called for FrameRenderable elements\n // - Layout stabilization complete\n // No additional RAF wait needed - can serialize immediately\n\n const t0 = performance.now();\n const dataUri = await captureTimelineToDataUri(renderClone, width, height, {\n renderContext,\n canvasScale: scale,\n timeMs,\n });\n const serializeTime = performance.now() - t0;\n\n const t1 = performance.now();\n const image = await loadImageFromDataUri(dataUri);\n const loadTime = performance.now() - t1;\n\n logger.debug(\n `[captureFromClone] foreignObject serialize=${serializeTime.toFixed(0)}ms, load=${loadTime.toFixed(0)}ms (canvasScale=${scale})`,\n );\n\n // Return image directly - no copy needed!\n return image;\n }\n } finally {\n // Ensure RenderContext is disposed even if an error occurs\n renderContext.dispose();\n }\n}\n\n/**\n * Captures a single frame from a timegroup at a specific time.\n *\n * CLONE-TIMELINE ARCHITECTURE:\n * Creates an independent render clone, seeks it to the target time, and captures.\n * Prime-timeline is NEVER seeked - user can continue previewing/editing during capture.\n *\n * @param timegroup - The source timegroup\n * @param options - Capture options including timeMs, scale, contentReadyMode\n * @returns Canvas with the rendered frame\n * @throws ContentNotReadyError if blocking mode times out waiting for video content\n */\nexport async function captureTimegroupAtTime(\n timegroup: EFTimegroup,\n options: CaptureOptions,\n): Promise<CanvasImageSource> {\n const {\n timeMs,\n scale = DEFAULT_CAPTURE_SCALE,\n // skipRestore is deprecated with Clone-timeline (Prime is never seeked)\n contentReadyMode = \"immediate\",\n blockingTimeoutMs = DEFAULT_BLOCKING_TIMEOUT_MS,\n canvasMode,\n skipClone = false,\n } = options;\n\n if (skipClone) {\n // DIRECT RENDERING: Skip clone creation for headless server rendering\n // Seek prime timeline directly and capture from it\n // WARNING: This modifies the prime timeline! Only use in headless contexts.\n\n const seekStart = performance.now();\n await timegroup.seekForRender(timeMs);\n const seekMs = performance.now() - seekStart;\n\n const renderStart = performance.now();\n // Use timegroup's actual container (parentElement or document.body as fallback)\n const container = (timegroup.parentElement || document.body) as HTMLElement;\n const result = await captureFromClone(timegroup, container, {\n scale,\n contentReadyMode,\n blockingTimeoutMs,\n originalTimegroup: undefined, // No original since we're rendering the prime\n canvasMode,\n timeMs, // Pass explicit time since we're not using a clone\n });\n const renderMs = performance.now() - renderStart;\n\n // Store timing (no clone time since we skipped it)\n if (typeof result === \"object\" && result !== null) {\n (result as any).__perfTiming = { cloneMs: 0, seekMs, renderMs };\n }\n\n return result;\n }\n\n // CLONE-TIMELINE: Create a short-lived render clone for this capture\n // Prime-timeline is NEVER seeked - clone is fully independent\n const cloneStart = performance.now();\n const {\n clone: renderClone,\n container: renderContainer,\n cleanup: cleanupRenderClone,\n } = await timegroup.createRenderClone();\n const cloneMs = performance.now() - cloneStart;\n\n try {\n // Seek the clone to target time (Prime stays at user position)\n // Use seekForRender which bypasses duration clamping - render clones may have\n // zero duration initially until media durations are computed, but we still\n // want to seek to the requested time for capture purposes.\n const seekStart = performance.now();\n await renderClone.seekForRender(timeMs);\n const seekMs = performance.now() - seekStart;\n\n // Use the shared capture helper\n const renderStart = performance.now();\n const result = await captureFromClone(renderClone, renderContainer, {\n scale,\n contentReadyMode,\n blockingTimeoutMs,\n originalTimegroup: timegroup,\n canvasMode,\n });\n const renderMs = performance.now() - renderStart;\n\n // Store timing on the result for access by callers (if they need it)\n // Note: CanvasImageSource doesn't support custom properties, but we can attach them anyway\n if (typeof result === \"object\" && result !== null) {\n (result as any).__perfTiming = { cloneMs, seekMs, renderMs };\n }\n\n return result;\n } finally {\n // Clean up the render clone\n cleanupRenderClone();\n }\n}\n\n/**\n * Generate thumbnails using an existing render clone and mutable queue.\n * The queue can be modified while generation is in progress.\n *\n * @param renderClone - Pre-created render clone to use\n * @param renderContainer - Container for the render clone\n * @param queue - Mutable queue that provides timestamps\n * @param options - Capture options (scale, contentReadyMode, etc.)\n * @yields Objects with { timeMs, canvas } for each captured thumbnail\n *\n * @example\n * ```ts\n * const queue = new MutableTimestampQueue();\n * queue.reset([0, 100, 200]);\n *\n * for await (const { timeMs, canvas } of generateThumbnailsFromClone(clone, container, queue)) {\n * cache.set(timeMs, canvas);\n * // Queue can be modified here while generator continues\n * }\n * ```\n */\nexport async function* generateThumbnailsFromClone(\n renderClone: EFTimegroup,\n renderContainer: HTMLElement,\n queue: ThumbnailQueue,\n options: GenerateThumbnailsOptions = {},\n): AsyncGenerator<GeneratedThumbnail> {\n const {\n scale = DEFAULT_CAPTURE_SCALE,\n contentReadyMode = \"immediate\",\n blockingTimeoutMs = DEFAULT_BLOCKING_TIMEOUT_MS,\n signal,\n } = options;\n\n while (true) {\n // Check if aborted before starting work\n if (signal?.aborted) {\n break;\n }\n\n const timeMs = queue.shift();\n if (timeMs === undefined) {\n // Queue is empty, generator exits\n break;\n }\n\n // Seek the clone to the target time\n await renderClone.seekForRender(timeMs);\n\n // Check if aborted after seek (before expensive capture)\n if (signal?.aborted) {\n break;\n }\n\n // Capture from the seeked clone, passing explicit timeMs\n const canvas = await captureFromClone(renderClone, renderContainer, {\n scale,\n contentReadyMode,\n blockingTimeoutMs,\n timeMs, // CRITICAL: Pass explicit time for accurate temporal visibility\n });\n\n // Yield the result with explicit timestamp association\n yield { timeMs, canvas };\n }\n}\n\n/**\n * Generate thumbnails for multiple timestamps efficiently using a single render clone.\n * This avoids the overhead of creating/destroying a clone for each thumbnail.\n *\n * @param timegroup - The timegroup to capture\n * @param timestamps - Array of timestamps to capture (in milliseconds)\n * @param options - Capture options (scale, contentReadyMode, etc.)\n * @param signal - Optional AbortSignal to cancel generation\n * @yields Objects with { timeMs, canvas } for each captured thumbnail\n *\n * @example\n * ```ts\n * for await (const { timeMs, canvas } of generateThumbnails(tg, [0, 100, 200])) {\n * console.log(`Got thumbnail for ${timeMs}ms`);\n * thumbnailCache.set(timeMs, canvas);\n * }\n * ```\n */\nexport async function* generateThumbnails(\n timegroup: EFTimegroup,\n timestamps: number[],\n options: GenerateThumbnailsOptions = {},\n signal?: AbortSignal,\n): AsyncGenerator<GeneratedThumbnail> {\n const {\n scale = DEFAULT_CAPTURE_SCALE,\n contentReadyMode = \"immediate\",\n blockingTimeoutMs = DEFAULT_BLOCKING_TIMEOUT_MS,\n } = options;\n\n // Create a single render clone for all thumbnails\n const {\n clone: renderClone,\n container: renderContainer,\n cleanup: cleanupRenderClone,\n } = await timegroup.createRenderClone();\n\n try {\n for (const timeMs of timestamps) {\n // Check for abort before each capture\n signal?.throwIfAborted();\n\n // Seek the clone to the target time\n await renderClone.seekForRender(timeMs);\n\n // Capture from the seeked clone\n const canvas = await captureFromClone(renderClone, renderContainer, {\n scale,\n contentReadyMode,\n blockingTimeoutMs,\n originalTimegroup: timegroup,\n });\n\n // Yield the result with explicit timestamp association\n yield { timeMs, canvas };\n }\n } finally {\n // Always clean up the render clone\n cleanupRenderClone();\n }\n}\n\n/** Epsilon for comparing time values (ms) - times within this are considered equal */\nconst TIME_EPSILON_MS = 1;\n\n/** Default scale for preview rendering */\nconst DEFAULT_PREVIEW_SCALE = 1;\n\n/** Default resolution scale (full resolution) */\nconst DEFAULT_RESOLUTION_SCALE = 1;\n\n/**\n * Convert relative time to absolute time for a timegroup.\n * Nested timegroup children have ABSOLUTE startTimeMs values,\n * so relative capture times must be converted for temporal culling.\n */\nfunction toAbsoluteTime(timegroup: EFTimegroup, relativeTimeMs: number): number {\n return relativeTimeMs + (timegroup.startTimeMs ?? 0);\n}\n\n/**\n * Renders a timegroup preview to a canvas using SVG foreignObject.\n *\n * Captures the prime timeline's current visual state including DOM changes\n * from frame tasks (SVG paths, canvas content, text updates, etc.).\n *\n * Optimized with:\n * - Passive clone structure rebuilt each frame from prime's current state\n * - Temporal bucketing for time-based culling\n * - RenderContext for canvas pixel caching across frames\n * - Resolution scaling for performance (renders at lower resolution, CSS upscales)\n *\n * @param timegroup - The source timegroup to preview (prime timeline)\n * @param scaleOrOptions - Scale factor (default 1) or options object\n * @returns Object with canvas and refresh function\n */\nexport function renderTimegroupToCanvas(\n timegroup: EFTimegroup,\n scaleOrOptions: number | CanvasPreviewOptions = DEFAULT_PREVIEW_SCALE,\n): CanvasPreviewResult {\n // Normalize options\n const options: CanvasPreviewOptions =\n typeof scaleOrOptions === \"number\" ? { scale: scaleOrOptions } : scaleOrOptions;\n\n const scale = options.scale ?? DEFAULT_PREVIEW_SCALE;\n // These are mutable to support dynamic resolution changes\n let currentResolutionScale = options.resolutionScale ?? DEFAULT_RESOLUTION_SCALE;\n\n const width = timegroup.offsetWidth || DEFAULT_WIDTH;\n const height = timegroup.offsetHeight || DEFAULT_HEIGHT;\n const dpr = (typeof window !== \"undefined\" ? window.devicePixelRatio : 1) || 1;\n\n // Calculate effective render dimensions (internal resolution) - mutable\n let renderWidth = Math.floor(width * currentResolutionScale);\n let renderHeight = Math.floor(height * currentResolutionScale);\n\n // Create canvas with proper DPR handling\n const canvas = createDprCanvas({\n renderWidth,\n renderHeight,\n scale,\n fullWidth: width,\n fullHeight: height,\n dpr,\n });\n\n // Return canvas directly - no wrapper needed\n const wrapperContainer = canvas;\n\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) {\n throw new Error(\"Failed to get canvas 2d context\");\n }\n\n // Track render state\n let rendering = false;\n let lastTimeMs = -1;\n let disposed = false;\n\n // Invalidate lastTimeMs when composition structure or attributes change so\n // refresh() re-renders even when currentTimeMs hasn't changed (e.g. paused edits).\n const compositionObserver = new MutationObserver(() => {\n if (!rendering) lastTimeMs = -1;\n });\n compositionObserver.observe(timegroup, {\n attributes: true,\n childList: true,\n subtree: true,\n });\n\n // Create RenderContext for caching across refresh calls (foreignObject only)\n const renderContext = new RenderContext();\n\n // Create FrameController for coordinating element rendering\n // Cached for the lifetime of this preview instance\n const frameController = new FrameController(timegroup);\n\n // Log resolution scale on first render for debugging\n let hasLoggedScale = false;\n\n // Pending resolution change - applied at start of next refresh to avoid blanking\n let pendingResolutionScale: number | null = null;\n\n // Use the user's render mode preference. Native requires the timegroup to be\n // inside a <canvas layoutsubtree> for drawElementImage to work.\n const useNative = getRenderMode() === \"native\" && isNativeCanvasApiAvailable();\n let captureCanvas: HTMLCanvasElement | null = null;\n let captureCtx: HtmlInCanvasContext | null = null;\n let originalParent: ParentNode | null = null;\n let originalNextSibling: ChildNode | null = null;\n let savedClipPath = \"\";\n let savedPointerEvents = \"\";\n\n if (useNative) {\n captureCanvas = document.createElement(\"canvas\");\n captureCanvas.setAttribute(\"layoutsubtree\", \"\");\n (captureCanvas as HtmlInCanvasElement).layoutSubtree = true;\n captureCanvas.width = renderWidth;\n captureCanvas.height = renderHeight;\n captureCanvas.style.cssText = `position:fixed;left:0;top:0;width:${width}px;height:${height}px;opacity:0;pointer-events:none;z-index:-9999;`;\n originalParent = timegroup.parentNode;\n originalNextSibling = timegroup.nextSibling;\n savedClipPath = timegroup.style.clipPath;\n savedPointerEvents = timegroup.style.pointerEvents;\n timegroup.style.clipPath = \"\";\n timegroup.style.pointerEvents = \"\";\n captureCanvas.appendChild(timegroup);\n document.body.appendChild(captureCanvas);\n captureCtx = captureCanvas.getContext(\"2d\") as HtmlInCanvasContext;\n void captureCanvas.offsetHeight;\n void timegroup.offsetHeight;\n }\n\n /**\n * Apply pending resolution scale changes.\n * Called at the start of refresh() before rendering, so the old content\n * stays visible until new content is ready to be drawn.\n */\n const applyPendingResolutionChange = (): void => {\n if (pendingResolutionScale === null) return;\n\n const newScale = pendingResolutionScale;\n pendingResolutionScale = null;\n\n currentResolutionScale = newScale;\n renderWidth = Math.floor(width * currentResolutionScale);\n renderHeight = Math.floor(height * currentResolutionScale);\n\n if (captureCanvas) {\n captureCanvas.width = renderWidth;\n captureCanvas.height = renderHeight;\n }\n };\n\n /**\n * Dynamically change resolution scale without rebuilding clone structure.\n * The actual change is deferred until next refresh() to avoid blanking -\n * old content stays visible until new content is ready.\n */\n const setResolutionScale = (newScale: number): void => {\n // Clamp to valid range\n newScale = Math.max(0.1, Math.min(1, newScale));\n\n if (newScale === currentResolutionScale && pendingResolutionScale === null) return;\n\n // Queue the change - will be applied at start of next refresh\n pendingResolutionScale = newScale;\n\n // Force re-render on next refresh by invalidating lastTimeMs\n lastTimeMs = -1;\n };\n\n const getResolutionScale = (): number => pendingResolutionScale ?? currentResolutionScale;\n\n const refresh = async (): Promise<void> => {\n if (disposed) return;\n\n const sourceTimeMs = timegroup.currentTimeMs ?? 0;\n const userTimeMs = timegroup.userTimeMs ?? 0;\n\n if (Math.abs(sourceTimeMs - userTimeMs) > TIME_EPSILON_MS) return;\n if (userTimeMs === lastTimeMs) return;\n if (rendering) return;\n\n lastTimeMs = userTimeMs;\n rendering = true;\n\n applyPendingResolutionChange();\n\n if (!hasLoggedScale) {\n hasLoggedScale = true;\n const mode = useNative ? \"native\" : \"foreignObject\";\n logger.debug(\n `[renderTimegroupToCanvas] Resolution scale: ${currentResolutionScale} (${width}x${height} → ${renderWidth}x${renderHeight}), canvas buffer: ${canvas.width}x${canvas.height}, CSS size: ${canvas.style.width}x${canvas.style.height}, renderMode: ${mode}`,\n );\n }\n\n try {\n await frameController.renderFrame(userTimeMs, {\n waitForLitUpdate: false,\n onAnimationsUpdate: (root) => {\n updateAnimations(root as AnimatableElement);\n },\n });\n\n if (useNative && captureCanvas && captureCtx) {\n if (captureCanvas.width !== width || captureCanvas.height !== height) {\n captureCtx.save();\n captureCtx.scale(captureCanvas.width / width, captureCanvas.height / height);\n captureCtx.drawElementImage(timegroup, 0, 0);\n captureCtx.restore();\n } else {\n captureCtx.drawElementImage(timegroup, 0, 0);\n }\n const targetWidth = Math.floor(renderWidth * scale * dpr);\n const targetHeight = Math.floor(renderHeight * scale * dpr);\n if (canvas.width !== targetWidth || canvas.height !== targetHeight) {\n canvas.width = targetWidth;\n canvas.height = targetHeight;\n } else {\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n }\n ctx.drawImage(captureCanvas, 0, 0, canvas.width, canvas.height);\n\n defaultProfiler.incrementRenderCount();\n } else {\n const absoluteTimeMs = toAbsoluteTime(timegroup, userTimeMs);\n\n const dataUri = await captureTimelineToDataUri(timegroup, width, height, {\n renderContext,\n canvasScale: currentResolutionScale,\n timeMs: absoluteTimeMs,\n });\n const image = await loadImageFromDataUri(dataUri);\n\n const targetWidth = Math.floor(renderWidth * scale * dpr);\n const targetHeight = Math.floor(renderHeight * scale * dpr);\n if (canvas.width !== targetWidth || canvas.height !== targetHeight) {\n canvas.width = targetWidth;\n canvas.height = targetHeight;\n } else {\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n }\n\n ctx.save();\n ctx.scale(dpr * scale, dpr * scale);\n ctx.drawImage(image, 0, 0, renderWidth, renderHeight);\n ctx.restore();\n\n defaultProfiler.incrementRenderCount();\n }\n } catch (e) {\n logger.error(\"Canvas preview render failed:\", e);\n } finally {\n rendering = false;\n }\n };\n\n /**\n * Dispose the preview and release resources.\n */\n const dispose = (): void => {\n if (disposed) return;\n disposed = true;\n compositionObserver.disconnect();\n frameController.abort();\n renderContext.dispose();\n\n // Restore timegroup to original DOM position if native mode moved it\n if (useNative && originalParent) {\n timegroup.style.clipPath = savedClipPath;\n timegroup.style.pointerEvents = savedPointerEvents;\n if (originalNextSibling) {\n originalParent.insertBefore(timegroup, originalNextSibling);\n } else {\n originalParent.appendChild(timegroup);\n }\n captureCanvas?.remove();\n }\n };\n\n // Do initial render\n refresh();\n\n return {\n container: wrapperContainer,\n canvas,\n refresh,\n setResolutionScale,\n getResolutionScale,\n dispose,\n };\n}\n"],"mappings":"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"}
@@ -99,9 +99,14 @@ async function renderToImageNative(container, width, height, options = {}) {
99
99
  defaultProfiler.addTime("setup", t1 - t0);
100
100
  try {
101
101
  getComputedStyle(container).opacity;
102
- if (reuseCanvas && captureCanvas.layoutSubtree && !_layoutInitializedCanvases.has(captureCanvas)) {
102
+ if (reuseCanvas) {
103
+ if (captureCanvas.layoutSubtree && !_layoutInitializedCanvases.has(captureCanvas)) {
104
+ await waitForFrame();
105
+ _layoutInitializedCanvases.add(captureCanvas);
106
+ if (!captureCanvas.parentNode) return captureCanvas;
107
+ }
108
+ } else {
103
109
  await waitForFrame();
104
- _layoutInitializedCanvases.add(captureCanvas);
105
110
  if (!captureCanvas.parentNode) return captureCanvas;
106
111
  }
107
112
  captureCanvas.getContext("2d").drawElementImage(container, 0, 0);
@@ -1 +1 @@
1
- {"version":3,"file":"renderToImageNative.js","names":["captureCanvas: HTMLCanvasElement","dpr"],"sources":["../../../src/preview/rendering/renderToImageNative.ts"],"sourcesContent":["/**\n * Native HTML-in-Canvas rendering using drawElementImage API.\n */\n\nimport type { HtmlInCanvasContext, HtmlInCanvasElement, NativeRenderOptions } from \"./types.js\";\nimport { defaultProfiler } from \"../RenderProfiler.js\";\n\n/** Track canvases that have been initialized for layoutsubtree (only need to wait once) */\nconst _layoutInitializedCanvases = new WeakSet<HTMLCanvasElement>();\n\n/**\n * Wait for next animation frame (allows browser to complete layout)\n */\nfunction waitForFrame(): Promise<void> {\n return new Promise((resolve) => requestAnimationFrame(() => resolve()));\n}\n\n/**\n * Create a canvas element with proper DPR handling.\n * Buffer size is based on renderWidth/renderHeight (internal resolution).\n * CSS size is based on fullWidth/fullHeight (logical display size).\n */\nexport function createDprCanvas(options: {\n renderWidth: number;\n renderHeight: number;\n scale: number;\n dpr?: number;\n fullWidth: number;\n fullHeight: number;\n}): HTMLCanvasElement {\n const { renderWidth, renderHeight, scale, fullWidth, fullHeight } = options;\n const dpr = options.dpr ?? window.devicePixelRatio ?? 1;\n\n const canvas = document.createElement(\"canvas\");\n canvas.width = Math.floor(renderWidth * scale * dpr);\n canvas.height = Math.floor(renderHeight * scale * dpr);\n canvas.style.width = `${Math.floor(fullWidth * scale)}px`;\n canvas.style.height = `${Math.floor(fullHeight * scale)}px`;\n\n return canvas;\n}\n\n/**\n * Render HTML content to canvas using native HTML-in-Canvas API (drawElementImage).\n * This is much faster than the foreignObject approach and avoids canvas tainting.\n *\n * Note: The native API renders at device pixel ratio, so we capture at DPR scale\n * and then downsample to logical pixels to match the foreignObject path's output.\n *\n * @param container - The HTML element to render\n * @param width - Target width in logical pixels\n * @param height - Target height in logical pixels\n * @param options - Rendering options (skipWait for batch mode)\n *\n * @see https://github.com/WICG/html-in-canvas\n */\nexport async function renderToImageNative(\n container: HTMLElement,\n width: number,\n height: number,\n options: NativeRenderOptions = {},\n): Promise<HTMLCanvasElement> {\n const t0 = performance.now();\n const { reuseCanvas, skipDprScaling = false } = options;\n // Use 1x DPR when skipDprScaling is true (for video export) - 4x fewer pixels!\n const dpr = skipDprScaling ? 1 : window.devicePixelRatio || 1;\n\n // Use provided canvas or create new one\n let captureCanvas: HTMLCanvasElement;\n let shouldCleanup = false;\n\n if (reuseCanvas) {\n captureCanvas = reuseCanvas;\n\n // Ensure canvas dimensions match (both attribute and CSS)\n const dpr = skipDprScaling ? 1 : window.devicePixelRatio || 1;\n const targetWidth = Math.floor(width * dpr);\n const targetHeight = Math.floor(height * dpr);\n\n // Set attribute dimensions (pixel buffer size)\n if (captureCanvas.width !== targetWidth) {\n captureCanvas.width = targetWidth;\n }\n if (captureCanvas.height !== targetHeight) {\n captureCanvas.height = targetHeight;\n }\n\n // Ensure CSS dimensions and positioning (same as non-reuse path)\n // This ensures consistent behavior and avoids layout issues\n captureCanvas.style.cssText = `\n position: fixed;\n left: 0;\n top: 0;\n width: ${width}px;\n height: ${height}px;\n opacity: 0;\n pointer-events: none;\n z-index: -9999;\n `;\n\n // Ensure layoutsubtree is set (required for drawElementImage)\n if (!captureCanvas.hasAttribute(\"layoutsubtree\")) {\n captureCanvas.setAttribute(\"layoutsubtree\", \"\");\n (captureCanvas as HtmlInCanvasElement).layoutSubtree = true;\n }\n\n // Ensure canvas is in DOM (required for drawElementImage layout)\n if (!captureCanvas.parentNode) {\n document.body.appendChild(captureCanvas);\n }\n\n // Ensure container is child of canvas\n if (container.parentElement !== captureCanvas) {\n captureCanvas.appendChild(container);\n }\n\n // Ensure container is visible (not display: none) for layout\n // drawElementImage requires the element to be laid out\n const containerStyle = getComputedStyle(container);\n if (containerStyle.display === \"none\") {\n container.style.display = \"block\";\n }\n\n // Force synchronous layout ONLY on first use with this canvas\n // For batch rendering (video export), repeated layout forces are expensive\n // We only need to force layout once to ensure everything is ready\n if (!_layoutInitializedCanvases.has(captureCanvas)) {\n void captureCanvas.offsetHeight;\n void container.offsetHeight;\n void getComputedStyle(captureCanvas).opacity;\n void getComputedStyle(container).opacity;\n _layoutInitializedCanvases.add(captureCanvas);\n }\n } else {\n captureCanvas = document.createElement(\"canvas\");\n captureCanvas.width = Math.floor(width * dpr);\n captureCanvas.height = Math.floor(height * dpr);\n\n // Enable HTML-in-Canvas mode via layoutsubtree attribute/property\n captureCanvas.setAttribute(\"layoutsubtree\", \"\");\n (captureCanvas as HtmlInCanvasElement).layoutSubtree = true;\n\n captureCanvas.appendChild(container);\n\n captureCanvas.style.cssText = `\n position: fixed;\n left: 0;\n top: 0;\n width: ${width}px;\n height: ${height}px;\n opacity: 0;\n pointer-events: none;\n z-index: -9999;\n `;\n document.body.appendChild(captureCanvas);\n shouldCleanup = true;\n }\n\n const t1 = performance.now();\n defaultProfiler.addTime(\"setup\", t1 - t0);\n\n try {\n // Force style calculation to ensure CSS is computed before capture\n // This ensures both canvas and container are laid out (required for drawElementImage)\n void getComputedStyle(container).opacity;\n\n // When reusing canvas with layoutsubtree, wait for initial layout (first use only)\n // Use a WeakSet to track canvases that have been initialized\n if (\n reuseCanvas &&\n (captureCanvas as any).layoutSubtree &&\n !_layoutInitializedCanvases.has(captureCanvas)\n ) {\n await waitForFrame();\n _layoutInitializedCanvases.add(captureCanvas);\n\n // Canvas may have been detached during async wait (e.g., test cleanup)\n if (!captureCanvas.parentNode) {\n return captureCanvas;\n }\n }\n\n const ctx = captureCanvas.getContext(\"2d\") as HtmlInCanvasContext;\n ctx.drawElementImage(container, 0, 0);\n } finally {\n // Only clean up if we created the canvas\n if (shouldCleanup && captureCanvas.parentNode) {\n captureCanvas.parentNode.removeChild(captureCanvas);\n }\n }\n\n const t2 = performance.now();\n defaultProfiler.addTime(\"draw\", t2 - t1);\n\n // If DPR is 1, no downsampling needed - return as-is\n if (dpr === 1) {\n defaultProfiler.incrementRenderCount();\n return captureCanvas;\n }\n\n // Downsample to logical pixel dimensions to match foreignObject path output\n // This ensures consistent behavior regardless of which rendering path is used\n const outputCanvas = document.createElement(\"canvas\");\n outputCanvas.width = width;\n outputCanvas.height = height;\n\n const outputCtx = outputCanvas.getContext(\"2d\")!;\n // Draw the DPR-scaled capture onto the 1x output canvas\n outputCtx.drawImage(\n captureCanvas,\n 0,\n 0,\n captureCanvas.width,\n captureCanvas.height, // source (full DPR capture)\n 0,\n 0,\n width,\n height, // destination (logical pixels)\n );\n\n const t3 = performance.now();\n defaultProfiler.addTime(\"downsample\", t3 - t2);\n defaultProfiler.incrementRenderCount();\n\n return outputCanvas;\n}\n"],"mappings":";;;;AAQA,MAAM,6CAA6B,IAAI,SAA4B;;;;AAKnE,SAAS,eAA8B;AACrC,QAAO,IAAI,SAAS,YAAY,4BAA4B,SAAS,CAAC,CAAC;;;;;;;AAQzE,SAAgB,gBAAgB,SAOV;CACpB,MAAM,EAAE,aAAa,cAAc,OAAO,WAAW,eAAe;CACpE,MAAM,MAAM,QAAQ,OAAO,OAAO,oBAAoB;CAEtD,MAAM,SAAS,SAAS,cAAc,SAAS;AAC/C,QAAO,QAAQ,KAAK,MAAM,cAAc,QAAQ,IAAI;AACpD,QAAO,SAAS,KAAK,MAAM,eAAe,QAAQ,IAAI;AACtD,QAAO,MAAM,QAAQ,GAAG,KAAK,MAAM,YAAY,MAAM,CAAC;AACtD,QAAO,MAAM,SAAS,GAAG,KAAK,MAAM,aAAa,MAAM,CAAC;AAExD,QAAO;;;;;;;;;;;;;;;;AAiBT,eAAsB,oBACpB,WACA,OACA,QACA,UAA+B,EAAE,EACL;CAC5B,MAAM,KAAK,YAAY,KAAK;CAC5B,MAAM,EAAE,aAAa,iBAAiB,UAAU;CAEhD,MAAM,MAAM,iBAAiB,IAAI,OAAO,oBAAoB;CAG5D,IAAIA;CACJ,IAAI,gBAAgB;AAEpB,KAAI,aAAa;AACf,kBAAgB;EAGhB,MAAMC,QAAM,iBAAiB,IAAI,OAAO,oBAAoB;EAC5D,MAAM,cAAc,KAAK,MAAM,QAAQA,MAAI;EAC3C,MAAM,eAAe,KAAK,MAAM,SAASA,MAAI;AAG7C,MAAI,cAAc,UAAU,YAC1B,eAAc,QAAQ;AAExB,MAAI,cAAc,WAAW,aAC3B,eAAc,SAAS;AAKzB,gBAAc,MAAM,UAAU;;;;eAInB,MAAM;gBACL,OAAO;;;;;AAOnB,MAAI,CAAC,cAAc,aAAa,gBAAgB,EAAE;AAChD,iBAAc,aAAa,iBAAiB,GAAG;AAC/C,GAAC,cAAsC,gBAAgB;;AAIzD,MAAI,CAAC,cAAc,WACjB,UAAS,KAAK,YAAY,cAAc;AAI1C,MAAI,UAAU,kBAAkB,cAC9B,eAAc,YAAY,UAAU;AAMtC,MADuB,iBAAiB,UAAU,CAC/B,YAAY,OAC7B,WAAU,MAAM,UAAU;AAM5B,MAAI,CAAC,2BAA2B,IAAI,cAAc,EAAE;AAClD,GAAK,cAAc;AACnB,GAAK,UAAU;AACf,GAAK,iBAAiB,cAAc,CAAC;AACrC,GAAK,iBAAiB,UAAU,CAAC;AACjC,8BAA2B,IAAI,cAAc;;QAE1C;AACL,kBAAgB,SAAS,cAAc,SAAS;AAChD,gBAAc,QAAQ,KAAK,MAAM,QAAQ,IAAI;AAC7C,gBAAc,SAAS,KAAK,MAAM,SAAS,IAAI;AAG/C,gBAAc,aAAa,iBAAiB,GAAG;AAC/C,EAAC,cAAsC,gBAAgB;AAEvD,gBAAc,YAAY,UAAU;AAEpC,gBAAc,MAAM,UAAU;;;;eAInB,MAAM;gBACL,OAAO;;;;;AAKnB,WAAS,KAAK,YAAY,cAAc;AACxC,kBAAgB;;CAGlB,MAAM,KAAK,YAAY,KAAK;AAC5B,iBAAgB,QAAQ,SAAS,KAAK,GAAG;AAEzC,KAAI;AAGF,EAAK,iBAAiB,UAAU,CAAC;AAIjC,MACE,eACC,cAAsB,iBACvB,CAAC,2BAA2B,IAAI,cAAc,EAC9C;AACA,SAAM,cAAc;AACpB,8BAA2B,IAAI,cAAc;AAG7C,OAAI,CAAC,cAAc,WACjB,QAAO;;AAKX,EADY,cAAc,WAAW,KAAK,CACtC,iBAAiB,WAAW,GAAG,EAAE;WAC7B;AAER,MAAI,iBAAiB,cAAc,WACjC,eAAc,WAAW,YAAY,cAAc;;CAIvD,MAAM,KAAK,YAAY,KAAK;AAC5B,iBAAgB,QAAQ,QAAQ,KAAK,GAAG;AAGxC,KAAI,QAAQ,GAAG;AACb,kBAAgB,sBAAsB;AACtC,SAAO;;CAKT,MAAM,eAAe,SAAS,cAAc,SAAS;AACrD,cAAa,QAAQ;AACrB,cAAa,SAAS;AAItB,CAFkB,aAAa,WAAW,KAAK,CAErC,UACR,eACA,GACA,GACA,cAAc,OACd,cAAc,QACd,GACA,GACA,OACA,OACD;CAED,MAAM,KAAK,YAAY,KAAK;AAC5B,iBAAgB,QAAQ,cAAc,KAAK,GAAG;AAC9C,iBAAgB,sBAAsB;AAEtC,QAAO"}
1
+ {"version":3,"file":"renderToImageNative.js","names":["captureCanvas: HTMLCanvasElement","dpr"],"sources":["../../../src/preview/rendering/renderToImageNative.ts"],"sourcesContent":["/**\n * Native HTML-in-Canvas rendering using drawElementImage API.\n */\n\nimport type { HtmlInCanvasContext, HtmlInCanvasElement, NativeRenderOptions } from \"./types.js\";\nimport { defaultProfiler } from \"../RenderProfiler.js\";\n\n/** Track canvases that have been initialized for layoutsubtree (only need to wait once) */\nconst _layoutInitializedCanvases = new WeakSet<HTMLCanvasElement>();\n\n/**\n * Wait for next animation frame (allows browser to complete layout)\n */\nfunction waitForFrame(): Promise<void> {\n return new Promise((resolve) => requestAnimationFrame(() => resolve()));\n}\n\n/**\n * Create a canvas element with proper DPR handling.\n * Buffer size is based on renderWidth/renderHeight (internal resolution).\n * CSS size is based on fullWidth/fullHeight (logical display size).\n */\nexport function createDprCanvas(options: {\n renderWidth: number;\n renderHeight: number;\n scale: number;\n dpr?: number;\n fullWidth: number;\n fullHeight: number;\n}): HTMLCanvasElement {\n const { renderWidth, renderHeight, scale, fullWidth, fullHeight } = options;\n const dpr = options.dpr ?? window.devicePixelRatio ?? 1;\n\n const canvas = document.createElement(\"canvas\");\n canvas.width = Math.floor(renderWidth * scale * dpr);\n canvas.height = Math.floor(renderHeight * scale * dpr);\n canvas.style.width = `${Math.floor(fullWidth * scale)}px`;\n canvas.style.height = `${Math.floor(fullHeight * scale)}px`;\n\n return canvas;\n}\n\n/**\n * Render HTML content to canvas using native HTML-in-Canvas API (drawElementImage).\n * This is much faster than the foreignObject approach and avoids canvas tainting.\n *\n * Note: The native API renders at device pixel ratio, so we capture at DPR scale\n * and then downsample to logical pixels to match the foreignObject path's output.\n *\n * @param container - The HTML element to render\n * @param width - Target width in logical pixels\n * @param height - Target height in logical pixels\n * @param options - Rendering options (skipWait for batch mode)\n *\n * @see https://github.com/WICG/html-in-canvas\n */\nexport async function renderToImageNative(\n container: HTMLElement,\n width: number,\n height: number,\n options: NativeRenderOptions = {},\n): Promise<HTMLCanvasElement> {\n const t0 = performance.now();\n const { reuseCanvas, skipDprScaling = false } = options;\n // Use 1x DPR when skipDprScaling is true (for video export) - 4x fewer pixels!\n const dpr = skipDprScaling ? 1 : window.devicePixelRatio || 1;\n\n // Use provided canvas or create new one\n let captureCanvas: HTMLCanvasElement;\n let shouldCleanup = false;\n\n if (reuseCanvas) {\n captureCanvas = reuseCanvas;\n\n // Ensure canvas dimensions match (both attribute and CSS)\n const dpr = skipDprScaling ? 1 : window.devicePixelRatio || 1;\n const targetWidth = Math.floor(width * dpr);\n const targetHeight = Math.floor(height * dpr);\n\n // Set attribute dimensions (pixel buffer size)\n if (captureCanvas.width !== targetWidth) {\n captureCanvas.width = targetWidth;\n }\n if (captureCanvas.height !== targetHeight) {\n captureCanvas.height = targetHeight;\n }\n\n // Ensure CSS dimensions and positioning (same as non-reuse path)\n // This ensures consistent behavior and avoids layout issues\n captureCanvas.style.cssText = `\n position: fixed;\n left: 0;\n top: 0;\n width: ${width}px;\n height: ${height}px;\n opacity: 0;\n pointer-events: none;\n z-index: -9999;\n `;\n\n // Ensure layoutsubtree is set (required for drawElementImage)\n if (!captureCanvas.hasAttribute(\"layoutsubtree\")) {\n captureCanvas.setAttribute(\"layoutsubtree\", \"\");\n (captureCanvas as HtmlInCanvasElement).layoutSubtree = true;\n }\n\n // Ensure canvas is in DOM (required for drawElementImage layout)\n if (!captureCanvas.parentNode) {\n document.body.appendChild(captureCanvas);\n }\n\n // Ensure container is child of canvas\n if (container.parentElement !== captureCanvas) {\n captureCanvas.appendChild(container);\n }\n\n // Ensure container is visible (not display: none) for layout\n // drawElementImage requires the element to be laid out\n const containerStyle = getComputedStyle(container);\n if (containerStyle.display === \"none\") {\n container.style.display = \"block\";\n }\n\n // Force synchronous layout ONLY on first use with this canvas\n // For batch rendering (video export), repeated layout forces are expensive\n // We only need to force layout once to ensure everything is ready\n if (!_layoutInitializedCanvases.has(captureCanvas)) {\n void captureCanvas.offsetHeight;\n void container.offsetHeight;\n void getComputedStyle(captureCanvas).opacity;\n void getComputedStyle(container).opacity;\n _layoutInitializedCanvases.add(captureCanvas);\n }\n } else {\n captureCanvas = document.createElement(\"canvas\");\n captureCanvas.width = Math.floor(width * dpr);\n captureCanvas.height = Math.floor(height * dpr);\n\n // Enable HTML-in-Canvas mode via layoutsubtree attribute/property\n captureCanvas.setAttribute(\"layoutsubtree\", \"\");\n (captureCanvas as HtmlInCanvasElement).layoutSubtree = true;\n\n captureCanvas.appendChild(container);\n\n captureCanvas.style.cssText = `\n position: fixed;\n left: 0;\n top: 0;\n width: ${width}px;\n height: ${height}px;\n opacity: 0;\n pointer-events: none;\n z-index: -9999;\n `;\n document.body.appendChild(captureCanvas);\n shouldCleanup = true;\n }\n\n const t1 = performance.now();\n defaultProfiler.addTime(\"setup\", t1 - t0);\n\n try {\n // Force style calculation to ensure CSS is computed before capture\n // This ensures both canvas and container are laid out (required for drawElementImage)\n void getComputedStyle(container).opacity;\n\n if (reuseCanvas) {\n // When reusing canvas with layoutsubtree, wait for initial layout (first use only)\n // Use a WeakSet to track canvases that have been initialized\n if ((captureCanvas as any).layoutSubtree && !_layoutInitializedCanvases.has(captureCanvas)) {\n await waitForFrame();\n _layoutInitializedCanvases.add(captureCanvas);\n\n // Canvas may have been detached during async wait (e.g., test cleanup)\n if (!captureCanvas.parentNode) {\n return captureCanvas;\n }\n }\n } else {\n // Single-shot capture: wait one frame so the browser composites the element\n // into the layoutsubtree canvas before drawElementImage reads GPU textures.\n // Without this, images loaded just before capture may render blank.\n await waitForFrame();\n\n // Canvas may have been detached during async wait (e.g., test cleanup)\n if (!captureCanvas.parentNode) {\n return captureCanvas;\n }\n }\n\n const ctx = captureCanvas.getContext(\"2d\") as HtmlInCanvasContext;\n ctx.drawElementImage(container, 0, 0);\n } finally {\n // Only clean up if we created the canvas\n if (shouldCleanup && captureCanvas.parentNode) {\n captureCanvas.parentNode.removeChild(captureCanvas);\n }\n }\n\n const t2 = performance.now();\n defaultProfiler.addTime(\"draw\", t2 - t1);\n\n // If DPR is 1, no downsampling needed - return as-is\n if (dpr === 1) {\n defaultProfiler.incrementRenderCount();\n return captureCanvas;\n }\n\n // Downsample to logical pixel dimensions to match foreignObject path output\n // This ensures consistent behavior regardless of which rendering path is used\n const outputCanvas = document.createElement(\"canvas\");\n outputCanvas.width = width;\n outputCanvas.height = height;\n\n const outputCtx = outputCanvas.getContext(\"2d\")!;\n // Draw the DPR-scaled capture onto the 1x output canvas\n outputCtx.drawImage(\n captureCanvas,\n 0,\n 0,\n captureCanvas.width,\n captureCanvas.height, // source (full DPR capture)\n 0,\n 0,\n width,\n height, // destination (logical pixels)\n );\n\n const t3 = performance.now();\n defaultProfiler.addTime(\"downsample\", t3 - t2);\n defaultProfiler.incrementRenderCount();\n\n return outputCanvas;\n}\n"],"mappings":"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"}
package/dist/style.css CHANGED
@@ -610,6 +610,9 @@ video {
610
610
  .shrink{
611
611
  flex-shrink: 1;
612
612
  }
613
+ .grow{
614
+ flex-grow: 1;
615
+ }
613
616
  .\!transform{
614
617
  transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)) !important;
615
618
  }
package/dist/version.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region src/version.ts
2
- const version = "0.46.1";
2
+ const version = "0.46.4";
3
3
  globalThis.__EF_VERSION__ = version;
4
4
 
5
5
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@editframe/elements",
3
- "version": "0.46.1",
3
+ "version": "0.46.4",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
@@ -24,7 +24,7 @@
24
24
  ],
25
25
  "dependencies": {
26
26
  "@bramus/style-observer": "^1.3.0",
27
- "@editframe/assets": "0.46.1",
27
+ "@editframe/assets": "0.46.4",
28
28
  "@lit/context": "^1.1.6",
29
29
  "@opentelemetry/api": "^1.9.0",
30
30
  "@opentelemetry/context-zone": "^1.26.0",