From c25e7cdcc2983bd4fbf155f6b37060d1b10aa33c Mon Sep 17 00:00:00 2001 From: Dmitri Date: Wed, 25 Mar 2026 13:18:03 +0400 Subject: [PATCH] fixed transition smoothing issue for presentations --- .../src/components/RuntimePresentation.tsx | 142 ++++++++---------- 1 file changed, 60 insertions(+), 82 deletions(-) diff --git a/frontend/src/components/RuntimePresentation.tsx b/frontend/src/components/RuntimePresentation.tsx index d1ae29f..5331e4c 100644 --- a/frontend/src/components/RuntimePresentation.tsx +++ b/frontend/src/components/RuntimePresentation.tsx @@ -24,7 +24,7 @@ import LayoutGuest from '../layouts/Guest'; import { getPageTitle } from '../config'; import { PRELOAD_CONFIG } from '../config/preload.config'; import { usePreloadOrchestrator } from '../hooks/usePreloadOrchestrator'; -import { useReversePlayback } from '../hooks/useReversePlayback'; +import { useTransitionPlayback } from '../hooks/useTransitionPlayback'; import { logger } from '../lib/logger'; import { resolveAssetPlaybackUrl } from '../lib/assetUrl'; import { buildElementStyle } from '../lib/elementStyles'; @@ -32,7 +32,6 @@ import type { RuntimeProject, RuntimePage, RuntimePageLink, - TransitionOverlayState, } from '../types/runtime'; interface RuntimePresentationProps { @@ -133,13 +132,16 @@ export default function RuntimePresentation({ const [pageLinks, setPageLinks] = useState([]); const [selectedPageId, setSelectedPageId] = useState(null); const [pageHistory, setPageHistory] = useState([]); - const [overlayTransition, setOverlayTransition] = - useState(null); + const [transitionPreview, setTransitionPreview] = useState<{ + targetPageId: string; + videoUrl: string; + isReverse: boolean; + } | null>(null); const [error, setError] = useState(''); const [isLoading, setIsLoading] = useState(true); const [isFullscreen, setIsFullscreen] = useState(false); - const overlayVideoRef = useRef(null); + const transitionVideoRef = useRef(null); // API request config with custom headers for project/environment const apiConfig = useMemo( @@ -212,6 +214,50 @@ export default function RuntimePresentation({ enabled: !isLoading && !error, }); + // Integrate useTransitionPlayback hook for smooth transitions (matches Constructor pattern) + const { isBuffering } = useTransitionPlayback({ + videoRef: transitionVideoRef, + transition: transitionPreview + ? { + videoUrl: transitionPreview.videoUrl, + reverseMode: transitionPreview.isReverse ? 'reverse' : 'none', + targetPageId: transitionPreview.targetPageId, + displayName: 'Transition', + } + : null, + onComplete: (targetPageId) => { + if (targetPageId) { + const targetPage = pages.find((p) => p.id === targetPageId); + waitForPageImages(targetPage || null).then(() => { + setSelectedPageId(targetPageId); + setPageHistory((prev) => [...prev, targetPageId]); + requestAnimationFrame(() => { + requestAnimationFrame(() => { + setTransitionPreview(null); + }); + }); + }); + } + }, + features: { + useBlobUrl: true, + preDecodeImages: true, + getTargetPageImages: () => { + if (!transitionPreview?.targetPageId) return []; + const targetPage = pages.find( + (p) => p.id === transitionPreview.targetPageId, + ); + if (!targetPage?.background_image_url) return []; + const url = resolveAssetPlaybackUrl(targetPage.background_image_url); + return url ? [url] : []; + }, + }, + preload: { + preloadedUrls: preloadOrchestrator?.preloadedUrls || new Set(), + getCachedBlobUrl: preloadOrchestrator?.getCachedBlobUrl, + }, + }); + const toggleFullscreen = useCallback(async () => { try { if (!document.fullscreenElement) { @@ -361,10 +407,9 @@ export default function RuntimePresentation({ if (!targetPage) return; if (transitionVideoUrl) { - // Play transition (forward or reverse) - setOverlayTransition({ + // Play transition using useTransitionPlayback hook + setTransitionPreview({ targetPageId, - transitionName: 'Transition', videoUrl: resolveAssetPlaybackUrl(transitionVideoUrl), isReverse: isBack, }); @@ -391,67 +436,6 @@ export default function RuntimePresentation({ [navigateToPage], ); - const finishOverlayTransition = useCallback(async () => { - if (!overlayTransition) return; - - const targetPage = pages.find( - (p) => p.id === overlayTransition.targetPageId, - ); - - // Wait for images while showing last frame - await waitForPageImages(targetPage || null); - - // Switch page - setSelectedPageId(overlayTransition.targetPageId); - setPageHistory((prev) => [...prev, overlayTransition.targetPageId]); - - // Hide transition after React renders - requestAnimationFrame(() => { - requestAnimationFrame(() => { - setOverlayTransition(null); - }); - }); - }, [overlayTransition, pages]); - - // Use the reverse playback hook (same as constructor.tsx) - const { startReverse, stopReverse } = useReversePlayback({ - videoRef: overlayVideoRef, - onComplete: finishOverlayTransition, - preloadedUrls: preloadOrchestrator.preloadedUrls, - videoUrl: overlayTransition?.videoUrl, - getCachedBlobUrl: preloadOrchestrator.getCachedBlobUrl, - }); - - // Handle reverse playback when transition starts in reverse mode - useEffect(() => { - if (!overlayTransition?.isReverse) return; - - const video = overlayVideoRef.current; - if (!video) return; - - // Start reverse when video data is ready (preloaded assets load instantly) - const handleLoadedData = () => { - startReverse(); - }; - - if (video.readyState >= 2) { - handleLoadedData(); - } else { - video.addEventListener('loadeddata', handleLoadedData, { once: true }); - } - - return () => { - stopReverse(); - video.removeEventListener('loadeddata', handleLoadedData); - }; - }, [ - overlayTransition?.isReverse, - overlayTransition?.videoUrl, - overlayTransition?.durationSec, - startReverse, - stopReverse, - ]); - // Render element content based on type const renderElementContent = (element: any) => { // Navigation buttons @@ -769,23 +753,17 @@ export default function RuntimePresentation({ - {/* Transition overlay */} - {overlayTransition && ( -
+ {/* Transition overlay - uses useTransitionPlayback hook for smooth transitions */} + {transitionPreview && ( +
)}