fixed transition smoothing issue for presentations
This commit is contained in:
parent
7f3b1795af
commit
c25e7cdcc2
@ -24,7 +24,7 @@ import LayoutGuest from '../layouts/Guest';
|
|||||||
import { getPageTitle } from '../config';
|
import { getPageTitle } from '../config';
|
||||||
import { PRELOAD_CONFIG } from '../config/preload.config';
|
import { PRELOAD_CONFIG } from '../config/preload.config';
|
||||||
import { usePreloadOrchestrator } from '../hooks/usePreloadOrchestrator';
|
import { usePreloadOrchestrator } from '../hooks/usePreloadOrchestrator';
|
||||||
import { useReversePlayback } from '../hooks/useReversePlayback';
|
import { useTransitionPlayback } from '../hooks/useTransitionPlayback';
|
||||||
import { logger } from '../lib/logger';
|
import { logger } from '../lib/logger';
|
||||||
import { resolveAssetPlaybackUrl } from '../lib/assetUrl';
|
import { resolveAssetPlaybackUrl } from '../lib/assetUrl';
|
||||||
import { buildElementStyle } from '../lib/elementStyles';
|
import { buildElementStyle } from '../lib/elementStyles';
|
||||||
@ -32,7 +32,6 @@ import type {
|
|||||||
RuntimeProject,
|
RuntimeProject,
|
||||||
RuntimePage,
|
RuntimePage,
|
||||||
RuntimePageLink,
|
RuntimePageLink,
|
||||||
TransitionOverlayState,
|
|
||||||
} from '../types/runtime';
|
} from '../types/runtime';
|
||||||
|
|
||||||
interface RuntimePresentationProps {
|
interface RuntimePresentationProps {
|
||||||
@ -133,13 +132,16 @@ export default function RuntimePresentation({
|
|||||||
const [pageLinks, setPageLinks] = useState<RuntimePageLink[]>([]);
|
const [pageLinks, setPageLinks] = useState<RuntimePageLink[]>([]);
|
||||||
const [selectedPageId, setSelectedPageId] = useState<string | null>(null);
|
const [selectedPageId, setSelectedPageId] = useState<string | null>(null);
|
||||||
const [pageHistory, setPageHistory] = useState<string[]>([]);
|
const [pageHistory, setPageHistory] = useState<string[]>([]);
|
||||||
const [overlayTransition, setOverlayTransition] =
|
const [transitionPreview, setTransitionPreview] = useState<{
|
||||||
useState<TransitionOverlayState | null>(null);
|
targetPageId: string;
|
||||||
|
videoUrl: string;
|
||||||
|
isReverse: boolean;
|
||||||
|
} | null>(null);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||||
|
|
||||||
const overlayVideoRef = useRef<HTMLVideoElement | null>(null);
|
const transitionVideoRef = useRef<HTMLVideoElement>(null);
|
||||||
|
|
||||||
// API request config with custom headers for project/environment
|
// API request config with custom headers for project/environment
|
||||||
const apiConfig = useMemo(
|
const apiConfig = useMemo(
|
||||||
@ -212,6 +214,50 @@ export default function RuntimePresentation({
|
|||||||
enabled: !isLoading && !error,
|
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 () => {
|
const toggleFullscreen = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
if (!document.fullscreenElement) {
|
if (!document.fullscreenElement) {
|
||||||
@ -361,10 +407,9 @@ export default function RuntimePresentation({
|
|||||||
if (!targetPage) return;
|
if (!targetPage) return;
|
||||||
|
|
||||||
if (transitionVideoUrl) {
|
if (transitionVideoUrl) {
|
||||||
// Play transition (forward or reverse)
|
// Play transition using useTransitionPlayback hook
|
||||||
setOverlayTransition({
|
setTransitionPreview({
|
||||||
targetPageId,
|
targetPageId,
|
||||||
transitionName: 'Transition',
|
|
||||||
videoUrl: resolveAssetPlaybackUrl(transitionVideoUrl),
|
videoUrl: resolveAssetPlaybackUrl(transitionVideoUrl),
|
||||||
isReverse: isBack,
|
isReverse: isBack,
|
||||||
});
|
});
|
||||||
@ -391,67 +436,6 @@ export default function RuntimePresentation({
|
|||||||
[navigateToPage],
|
[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
|
// Render element content based on type
|
||||||
const renderElementContent = (element: any) => {
|
const renderElementContent = (element: any) => {
|
||||||
// Navigation buttons
|
// Navigation buttons
|
||||||
@ -769,23 +753,17 @@ export default function RuntimePresentation({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Transition overlay */}
|
{/* Transition overlay - uses useTransitionPlayback hook for smooth transitions */}
|
||||||
{overlayTransition && (
|
{transitionPreview && (
|
||||||
<div className='absolute inset-0 z-40 bg-black'>
|
<div className='fixed inset-0 z-50 overflow-hidden pointer-events-none'>
|
||||||
<video
|
<video
|
||||||
ref={overlayVideoRef}
|
ref={transitionVideoRef}
|
||||||
className='w-full h-full object-cover'
|
className='absolute inset-0 h-full w-full object-cover transition-opacity duration-300 ease-linear'
|
||||||
src={overlayTransition.videoUrl}
|
style={{ opacity: isBuffering ? 0 : 1 }}
|
||||||
autoPlay={!overlayTransition.isReverse}
|
|
||||||
muted
|
muted
|
||||||
playsInline
|
playsInline
|
||||||
preload='auto'
|
preload='auto'
|
||||||
onEnded={
|
disablePictureInPicture
|
||||||
overlayTransition.isReverse
|
|
||||||
? undefined
|
|
||||||
: finishOverlayTransition
|
|
||||||
}
|
|
||||||
onError={finishOverlayTransition}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user