properly handle settings state on lyrics follow

This commit is contained in:
jeffvli
2026-07-09 03:59:20 -07:00
parent 04258061e2
commit c0c03bc414
3 changed files with 49 additions and 1 deletions
@@ -21,10 +21,14 @@ export interface UseLyricsAnimationEngineOptions {
enabled?: boolean;
followRef?: React.RefObject<boolean>;
followScrollAlignmentRef?: React.RefObject<number>;
fontSize?: number;
gap?: number;
lineIdPrefix: 'karaoke-line' | 'lyric';
lineLeadTimeMsRef?: React.RefObject<number>;
lyrics: SynchronizedLyrics;
onLineActive?: (lineIndex: number) => void;
paddingLeft?: number;
paddingRight?: number;
scrollContainerId: string;
}
@@ -34,10 +38,14 @@ export const useLyricsAnimationEngine = ({
enabled = true,
followRef,
followScrollAlignmentRef,
fontSize,
gap,
lineIdPrefix,
lineLeadTimeMsRef,
lyrics,
onLineActive,
paddingLeft,
paddingRight,
scrollContainerId,
}: UseLyricsAnimationEngineOptions) => {
const internalAnimStateRef = useRef<AnimEngineState>(createAnimEngineState());
@@ -83,6 +91,7 @@ export const useLyricsAnimationEngine = ({
const recalculatePositions = useCallback(() => {
if (lyricsDataRef.current) {
recalculateLinePositions(lyricsDataRef.current);
animStateRef.current.scroll.pendingScroll = true;
animStateRef.current.scroll.wasUserScrolling = true;
}
// eslint-disable-next-line react-hooks/exhaustive-deps -- animStateRef is read via .current at call time
@@ -149,20 +158,51 @@ export const useLyricsAnimationEngine = ({
};
}, [lyrics, enabled, rebuildLyricsData, reset]);
useEffect(() => {
const frame = requestAnimationFrame(() => {
recalculatePositions();
});
return () => {
cancelAnimationFrame(frame);
};
}, [fontSize, gap, paddingLeft, paddingRight, recalculatePositions]);
useEffect(() => {
const container = containerRef.current;
if (!container) {
return;
}
const observedElements = new Set<Element>();
const observer = new ResizeObserver(() => {
recalculatePositions();
});
observer.observe(container);
const observeLayoutTargets = () => {
if (!observedElements.has(container)) {
observer.observe(container);
observedElements.add(container);
}
const content = container.firstElementChild;
if (content && !observedElements.has(content)) {
observer.observe(content);
observedElements.add(content);
}
};
observeLayoutTargets();
const mutationObserver = new MutationObserver(() => {
observeLayoutTargets();
});
mutationObserver.observe(container, { childList: true });
return () => {
observer.disconnect();
mutationObserver.disconnect();
};
}, [containerRef, recalculatePositions]);
@@ -94,9 +94,13 @@ export const SynchronizedKaraokeLyrics = ({
containerRef,
followRef,
followScrollAlignmentRef,
fontSize: settings.fontSize,
gap: settings.gap,
lineIdPrefix: 'karaoke-line',
lineLeadTimeMsRef,
lyrics: normalizedLyrics,
paddingLeft: settings.paddingLeft,
paddingRight: settings.paddingRight,
scrollContainerId: LYRICS_SCROLL_CONTAINER_ID,
});
@@ -82,9 +82,13 @@ export const SynchronizedLyrics = ({
containerRef,
followRef,
followScrollAlignmentRef,
fontSize: settings.fontSize,
gap: settings.gap,
lineIdPrefix: 'lyric',
lineLeadTimeMsRef,
lyrics: normalizedLyrics,
paddingLeft: settings.paddingLeft,
paddingRight: settings.paddingRight,
scrollContainerId: LYRICS_SCROLL_CONTAINER_ID,
});