()
| 736 | }; |
| 737 | |
| 738 | const resolveRootTimelineFromDocument = (): TimelineResolution => { |
| 739 | const timelines = (window.__timelines ?? {}) as Record<string, RuntimeTimelineLike | undefined>; |
| 740 | // DX fallback (#6): when the root timeline cannot be resolved by id but |
| 741 | // EXACTLY ONE usable timeline is registered, bind it rather than silently |
| 742 | // rendering the frozen t=0 DOM. Safe because with a single registered |
| 743 | // timeline there is no ambiguity about which one is the composition's |
| 744 | // root. Multiple registered → ambiguous, so we still return null and let |
| 745 | // the loud warning fire. |
| 746 | const resolveSoleTimelineFallback = (reason: string): TimelineResolution => { |
| 747 | const usable = Object.entries(timelines).filter( |
| 748 | (entry): entry is [string, RuntimeTimelineLike] => |
| 749 | !!entry[1] && typeof entry[1].play === "function" && typeof entry[1].pause === "function", |
| 750 | ); |
| 751 | if (usable.length !== 1) return { timeline: null }; |
| 752 | const [soleId, soleTimeline] = usable[0]; |
| 753 | return { |
| 754 | timeline: soleTimeline, |
| 755 | selectedTimelineIds: [soleId], |
| 756 | selectedDurationSeconds: getTimelineDurationSeconds(soleTimeline), |
| 757 | diagnostics: { |
| 758 | code: "root_timeline_sole_registered_fallback", |
| 759 | details: { reason, soleTimelineId: soleId }, |
| 760 | }, |
| 761 | }; |
| 762 | }; |
| 763 | const startResolver = createRuntimeStartTimeResolver({ |
| 764 | timelineRegistry: timelines, |
| 765 | includeAuthoredTimingAttrs: true, |
| 766 | }); |
| 767 | const mediaDurationFloorSeconds = resolveMediaDurationFloorSeconds(); |
| 768 | const authoredCompositionDurationFloorSeconds = |
| 769 | resolveAuthoredCompositionDurationFloorSeconds(); |
| 770 | const durationFloorSeconds = |
| 771 | Math.max(mediaDurationFloorSeconds ?? 0, authoredCompositionDurationFloorSeconds ?? 0) || |
| 772 | null; |
| 773 | const minCandidateDurationSeconds = resolveMinCandidateDurationSeconds(durationFloorSeconds); |
| 774 | const resolveCompositionStartSeconds = (compositionId: string): number => { |
| 775 | const node = document.querySelector( |
| 776 | `[data-composition-id="${CSS.escape(compositionId)}"]`, |
| 777 | ) as Element | null; |
| 778 | if (!node) return 0; |
| 779 | return startResolver.resolveStartForElement(node, 0); |
| 780 | }; |
| 781 | const createCompositeTimelineFromCandidates = ( |
| 782 | candidates: Array<{ |
| 783 | compositionId: string; |
| 784 | timeline: RuntimeTimelineLike; |
| 785 | durationSeconds: number; |
| 786 | }>, |
| 787 | ): RuntimeTimelineLike | null => { |
| 788 | const gsapApi = window.gsap; |
| 789 | if (!gsapApi || typeof gsapApi.timeline !== "function") return null; |
| 790 | const compositeTimeline = gsapApi.timeline({ paused: true }) as RuntimeTimelineLike; |
| 791 | for (const candidate of candidates) { |
| 792 | compositeTimeline.add( |
| 793 | candidate.timeline, |
| 794 | resolveCompositionStartSeconds(candidate.compositionId), |
| 795 | ); |
no test coverage detected