fixed fades issue
This commit is contained in:
parent
804e082ed7
commit
28b6f8fe71
File diff suppressed because one or more lines are too long
@ -53,9 +53,11 @@ const TransitionPreviewOverlay: React.FC<TransitionPreviewOverlayProps> = ({
|
||||
className='overflow-hidden'
|
||||
style={letterboxStyles || { position: 'absolute', inset: 0 }}
|
||||
>
|
||||
{/* Video element - no opacity transition to ensure instant appearance
|
||||
when ready. The video itself IS the transition effect. */}
|
||||
<video
|
||||
ref={videoRef}
|
||||
className={`absolute inset-0 h-full w-full transition-opacity duration-300 ease-linear ${
|
||||
className={`absolute inset-0 h-full w-full ${
|
||||
videoFit === 'cover' ? 'object-cover' : 'object-contain'
|
||||
}`}
|
||||
style={{ opacity: videoOpacity }}
|
||||
|
||||
@ -108,6 +108,7 @@ export default function RuntimePresentation({
|
||||
} | null>(null);
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const [isBackgroundReady, setIsBackgroundReady] = useState(true);
|
||||
// Track when transition video has completed but we're waiting for background to load
|
||||
const [pendingTransitionComplete, setPendingTransitionComplete] =
|
||||
useState(false);
|
||||
const [activeGalleryCarousel, setActiveGalleryCarousel] = useState<{
|
||||
@ -212,7 +213,9 @@ export default function RuntimePresentation({
|
||||
applyPageSelection(targetPageId, isBack ?? false);
|
||||
});
|
||||
setIsBackgroundReady(false);
|
||||
// Signal that transition is complete and waiting for Image onLoad
|
||||
// Video transition completed - last frame shows new page background
|
||||
// Signal that we're waiting for background to load before removing overlay
|
||||
// Overlay will be removed instantly (no fade) when isBackgroundReady becomes true
|
||||
setPendingTransitionComplete(true);
|
||||
} else {
|
||||
// No target page - clean up and remove overlay
|
||||
@ -237,27 +240,17 @@ export default function RuntimePresentation({
|
||||
});
|
||||
|
||||
// Use shared background transition hook for crossfade effects
|
||||
const {
|
||||
isOverlayFadingOut,
|
||||
resetFadeOut,
|
||||
isFadingIn,
|
||||
onFadeInAnimationEnd,
|
||||
resetFadeIn,
|
||||
} = useBackgroundTransition({
|
||||
pageSwitch,
|
||||
fadeOut: {
|
||||
pendingTransitionComplete,
|
||||
isBackgroundReady,
|
||||
transitionVideoRef,
|
||||
onTransitionCleanup: useCallback(() => {
|
||||
setTransitionPreview(null);
|
||||
setPendingTransitionComplete(false);
|
||||
}, []),
|
||||
},
|
||||
fadeIn: {
|
||||
hasActiveTransition: Boolean(transitionPreview),
|
||||
},
|
||||
});
|
||||
// NOTE: fadeOut config is NOT used for video transitions.
|
||||
// Video transitions end instantly (last frame = new page, then overlay removed).
|
||||
// fadeIn is used for non-video navigation (crossfade 500ms).
|
||||
const { isFadingIn, onFadeInAnimationEnd, resetFadeIn } =
|
||||
useBackgroundTransition({
|
||||
pageSwitch,
|
||||
// No fadeOut - video transitions don't use fade
|
||||
fadeIn: {
|
||||
hasActiveTransition: Boolean(transitionPreview),
|
||||
},
|
||||
});
|
||||
|
||||
const toggleFullscreen = useCallback(async () => {
|
||||
try {
|
||||
@ -336,6 +329,21 @@ export default function RuntimePresentation({
|
||||
}
|
||||
}, [selectedPage?.background_image_url, selectedPage?.background_video_url]);
|
||||
|
||||
// Video transition overlay removal - instant (no fade) when background is ready
|
||||
// This ensures UI elements have time to appear before we remove the transition overlay
|
||||
useEffect(() => {
|
||||
if (pendingTransitionComplete && isBackgroundReady) {
|
||||
// Background is ready - instantly remove transition overlay (no fade)
|
||||
const video = transitionVideoRef.current;
|
||||
if (video) {
|
||||
video.removeAttribute('src');
|
||||
video.load();
|
||||
}
|
||||
setTransitionPreview(null);
|
||||
setPendingTransitionComplete(false);
|
||||
}
|
||||
}, [pendingTransitionComplete, isBackgroundReady]);
|
||||
|
||||
// Safari Black Flash Prevention:
|
||||
// Update lastKnownBgUrl when a background is successfully displayed.
|
||||
// This creates a "snapshot" that persists through transitions.
|
||||
@ -367,10 +375,8 @@ export default function RuntimePresentation({
|
||||
if (!targetPage) return;
|
||||
|
||||
if (transitionVideoUrl) {
|
||||
// Reset states from previous transition before starting new one
|
||||
// This prevents the fade-out/fade-in effects from re-triggering
|
||||
// Reset states from previous transition/navigation
|
||||
resetFadeIn();
|
||||
resetFadeOut();
|
||||
setPendingTransitionComplete(false);
|
||||
// Play transition using useTransitionPlayback hook
|
||||
setTransitionPreview({
|
||||
@ -397,7 +403,7 @@ export default function RuntimePresentation({
|
||||
});
|
||||
}
|
||||
},
|
||||
[pages, pageSwitch, resetFadeOut, resetFadeIn, applyPageSelection],
|
||||
[pages, pageSwitch, resetFadeIn, applyPageSelection],
|
||||
);
|
||||
|
||||
const handleElementClick = useCallback(
|
||||
@ -722,14 +728,15 @@ export default function RuntimePresentation({
|
||||
|
||||
{/* Transition overlay - uses useTransitionPlayback hook for smooth transitions */}
|
||||
{/* Opacity is 0 during 'preparing' phase to show old page while video loads */}
|
||||
{/* Also fades to 0 when isOverlayFadingOut to reveal the new page underneath */}
|
||||
{/* NO fade-out: video itself IS the transition (last frame = new page) */}
|
||||
{/* Overlay removed instantly via setTransitionPreview(null) in onComplete */}
|
||||
{transitionPreview && (
|
||||
<TransitionPreviewOverlay
|
||||
videoRef={transitionVideoRef}
|
||||
isActive={true}
|
||||
isBuffering={transitionPhase === 'preparing' || isBuffering}
|
||||
letterboxStyles={letterboxStyles}
|
||||
opacity={isOverlayFadingOut ? 0 : 1}
|
||||
opacity={1}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@ -43,18 +43,13 @@ export const CANVAS_CONFIG = {
|
||||
|
||||
// Page transition effects
|
||||
pageTransition: {
|
||||
/**
|
||||
* Fade-out duration for transition video overlay (ms).
|
||||
* Applied after transition video finishes playing.
|
||||
*/
|
||||
fadeOutDurationMs: 300,
|
||||
/**
|
||||
* Crossfade animation duration for page backgrounds (ms).
|
||||
* Used for smooth transitions between pages.
|
||||
*/
|
||||
crossfadeDurationMs: 300,
|
||||
/**
|
||||
* CSS easing function for fade animations.
|
||||
* Duration is controlled by CSS variable --crossfade-duration (single source of truth).
|
||||
* Use getCrossfadeDuration() from browserUtils.ts to read the value in JS.
|
||||
*
|
||||
* NOTE: Video transitions do NOT use fade - video itself is the transition.
|
||||
* Crossfade only applies to non-video page navigation.
|
||||
*/
|
||||
easing: 'ease-out' as const,
|
||||
},
|
||||
|
||||
@ -12,6 +12,11 @@
|
||||
@import "_theme.css";
|
||||
@import '_rich-text.css';
|
||||
|
||||
/* Page transition timing - single source of truth */
|
||||
:root {
|
||||
--crossfade-duration: 700ms;
|
||||
}
|
||||
|
||||
|
||||
.introjs-tooltip {
|
||||
@apply min-w-[400px] max-w-[480px] p-2 !important;
|
||||
@ -84,13 +89,14 @@
|
||||
}
|
||||
|
||||
/* Crossfade animation classes - GPU accelerated for Safari */
|
||||
/* Duration controlled by --crossfade-duration CSS variable (single source of truth) */
|
||||
.animate-crossfade-in {
|
||||
/* Explicit initial state prevents Safari flash during animation setup */
|
||||
opacity: 0;
|
||||
-webkit-transform: translate3d(0, 0, 0);
|
||||
transform: translate3d(0, 0, 0);
|
||||
-webkit-animation: page-crossfade-in 300ms ease-out forwards;
|
||||
animation: page-crossfade-in 300ms ease-out forwards;
|
||||
-webkit-animation: page-crossfade-in var(--crossfade-duration, 700ms) ease-out forwards;
|
||||
animation: page-crossfade-in var(--crossfade-duration, 700ms) ease-out forwards;
|
||||
-webkit-backface-visibility: hidden;
|
||||
backface-visibility: hidden;
|
||||
/* Only animate opacity - transform is for GPU layer promotion */
|
||||
@ -102,8 +108,8 @@
|
||||
opacity: 1;
|
||||
-webkit-transform: translate3d(0, 0, 0);
|
||||
transform: translate3d(0, 0, 0);
|
||||
-webkit-animation: page-crossfade-out 300ms ease-out forwards;
|
||||
animation: page-crossfade-out 300ms ease-out forwards;
|
||||
-webkit-animation: page-crossfade-out var(--crossfade-duration, 700ms) ease-out forwards;
|
||||
animation: page-crossfade-out var(--crossfade-duration, 700ms) ease-out forwards;
|
||||
-webkit-backface-visibility: hidden;
|
||||
backface-visibility: hidden;
|
||||
/* Only animate opacity - transform is for GPU layer promotion */
|
||||
|
||||
@ -2,16 +2,18 @@
|
||||
* useBackgroundTransition Hook
|
||||
*
|
||||
* Manages background transition effects when switching between pages.
|
||||
* Handles the fade-out animation of the transition video overlay,
|
||||
* fade-in animation for non-transition navigation, and
|
||||
* Handles crossfade animation for non-video navigation and
|
||||
* coordinates with the page switch hook to clear previous backgrounds.
|
||||
*
|
||||
* This hook consolidates the background transition logic used by both
|
||||
* RuntimePresentation and constructor.tsx.
|
||||
*
|
||||
* NOTE: Video transitions do NOT use any fades - the video itself IS the transition.
|
||||
* Video last frame = new page background, then overlay is removed instantly.
|
||||
*
|
||||
* Two modes:
|
||||
* 1. Full mode (RuntimePresentation): Fade-out animation + fade-in + direct navigation clearing
|
||||
* 2. Simple mode (constructor): Direct navigation clearing + optional fade-in
|
||||
* 1. fadeIn mode: Crossfade animation for direct (non-video) page navigation
|
||||
* 2. fadeOut mode: Legacy support - kept for backwards compatibility but not recommended
|
||||
*/
|
||||
|
||||
import {
|
||||
@ -21,13 +23,11 @@ import {
|
||||
useCallback,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { CANVAS_CONFIG } from '../config/canvas.config';
|
||||
import { isSafari, scheduleAfterPaintSafari } from '../lib/browserUtils';
|
||||
|
||||
/**
|
||||
* Fade-out duration from config (for transition video overlay)
|
||||
*/
|
||||
const FADE_OUT_DURATION_MS = CANVAS_CONFIG.pageTransition.fadeOutDurationMs;
|
||||
import {
|
||||
isSafari,
|
||||
scheduleAfterPaintSafari,
|
||||
getCrossfadeDuration,
|
||||
} from '../lib/browserUtils';
|
||||
|
||||
/**
|
||||
* Fade-out configuration (optional - for RuntimePresentation)
|
||||
@ -194,6 +194,7 @@ export function useBackgroundTransition({
|
||||
const { transitionVideoRef, onTransitionCleanup } = fadeOut;
|
||||
|
||||
// After fade completes, remove the overlay
|
||||
// Duration is read from CSS variable for consistency
|
||||
const fadeTimer = setTimeout(() => {
|
||||
const video = transitionVideoRef.current;
|
||||
if (video) {
|
||||
@ -209,7 +210,7 @@ export function useBackgroundTransition({
|
||||
|
||||
// Reset fade-out state
|
||||
setIsOverlayFadingOut(false);
|
||||
}, FADE_OUT_DURATION_MS);
|
||||
}, getCrossfadeDuration());
|
||||
|
||||
return () => clearTimeout(fadeTimer);
|
||||
}, [fadeOut, isOverlayFadingOut, pageSwitch]);
|
||||
|
||||
@ -166,6 +166,30 @@ export const scheduleAfterPaint = (callback: () => void): void => {
|
||||
}, 0);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get crossfade duration from CSS custom property.
|
||||
* Single source of truth: CSS variable --crossfade-duration in main.css.
|
||||
*
|
||||
* @returns Duration in milliseconds (default: 500ms)
|
||||
*/
|
||||
export const getCrossfadeDuration = (): number => {
|
||||
if (typeof window === 'undefined') return 700; // SSR fallback
|
||||
|
||||
const root = document.documentElement;
|
||||
const value = getComputedStyle(root)
|
||||
.getPropertyValue('--crossfade-duration')
|
||||
.trim();
|
||||
|
||||
// Parse "700ms" or "0.7s" to milliseconds
|
||||
if (value.endsWith('ms')) {
|
||||
return parseInt(value, 10) || 700;
|
||||
}
|
||||
if (value.endsWith('s')) {
|
||||
return (parseFloat(value) || 0.7) * 1000;
|
||||
}
|
||||
return 700; // fallback
|
||||
};
|
||||
|
||||
/**
|
||||
* Schedule a callback with explicit frame delay.
|
||||
* Useful when you need to ensure a specific number of frames have passed.
|
||||
|
||||
@ -219,6 +219,10 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
||||
element: CanvasElement;
|
||||
initialIndex: number;
|
||||
} | null>(null);
|
||||
// Track background ready state for smooth video transition completion
|
||||
const [isBackgroundReady, setIsBackgroundReady] = useState(true);
|
||||
const [pendingTransitionComplete, setPendingTransitionComplete] =
|
||||
useState(false);
|
||||
|
||||
const isConstructorEditMode = constructorInteractionMode === 'edit';
|
||||
const allowedNavigationTypes = useMemo<NavigationElementType[]>(() => {
|
||||
@ -368,6 +372,36 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
||||
},
|
||||
});
|
||||
|
||||
// Video transition overlay removal - instant (no fade) when background is ready
|
||||
// This ensures UI elements have time to appear before we remove the transition overlay
|
||||
useEffect(() => {
|
||||
if (pendingTransitionComplete && isBackgroundReady) {
|
||||
// Background is ready - instantly remove transition overlay (no fade)
|
||||
const video = transitionVideoRef.current;
|
||||
if (video) {
|
||||
video.removeAttribute('src');
|
||||
video.load();
|
||||
}
|
||||
closeTransitionPreview();
|
||||
setPendingTransitionComplete(false);
|
||||
}
|
||||
}, [pendingTransitionComplete, isBackgroundReady, closeTransitionPreview]);
|
||||
|
||||
// Handle background ready state for pages without images or with videos
|
||||
useEffect(() => {
|
||||
// If no background image, or if there's a video (video takes over), mark as ready
|
||||
if (!activePage?.background_image_url || activePage?.background_video_url) {
|
||||
setIsBackgroundReady(true);
|
||||
}
|
||||
}, [activePage?.background_image_url, activePage?.background_video_url]);
|
||||
|
||||
// Reset pending state when starting a new transition
|
||||
useEffect(() => {
|
||||
if (transitionPreview) {
|
||||
setPendingTransitionComplete(false);
|
||||
}
|
||||
}, [transitionPreview]);
|
||||
|
||||
// Helper to switch pages without flash
|
||||
// Uses usePageSwitch hook to resolve blob URLs from preload cache
|
||||
// Also updates storage path state for editing/saving purposes
|
||||
@ -430,17 +464,15 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
||||
clearSelection();
|
||||
setSelectedMenuItem('none');
|
||||
setErrorMessage('');
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
video?.removeAttribute('src');
|
||||
video?.load();
|
||||
closeTransitionPreview();
|
||||
});
|
||||
});
|
||||
setIsBackgroundReady(false);
|
||||
// Signal that transition video completed - wait for background to load
|
||||
// Overlay will be removed instantly when isBackgroundReady becomes true
|
||||
setPendingTransitionComplete(true);
|
||||
} else {
|
||||
video?.removeAttribute('src');
|
||||
video?.load();
|
||||
closeTransitionPreview();
|
||||
setPendingTransitionComplete(false);
|
||||
}
|
||||
},
|
||||
timeouts: {
|
||||
@ -1459,7 +1491,10 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
||||
isSwitching={pageSwitch.isSwitching}
|
||||
isNewBgReady={pageSwitch.isNewBgReady}
|
||||
isFadingIn={isFadingIn}
|
||||
onBackgroundReady={() => pageSwitch.markBackgroundReady()}
|
||||
onBackgroundReady={() => {
|
||||
pageSwitch.markBackgroundReady();
|
||||
setIsBackgroundReady(true);
|
||||
}}
|
||||
videoAutoplay={backgroundVideoAutoplay}
|
||||
videoLoop={backgroundVideoLoop}
|
||||
videoMuted={backgroundVideoMuted}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user