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'
|
className='overflow-hidden'
|
||||||
style={letterboxStyles || { position: 'absolute', inset: 0 }}
|
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
|
<video
|
||||||
ref={videoRef}
|
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'
|
videoFit === 'cover' ? 'object-cover' : 'object-contain'
|
||||||
}`}
|
}`}
|
||||||
style={{ opacity: videoOpacity }}
|
style={{ opacity: videoOpacity }}
|
||||||
|
|||||||
@ -108,6 +108,7 @@ export default function RuntimePresentation({
|
|||||||
} | null>(null);
|
} | null>(null);
|
||||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||||
const [isBackgroundReady, setIsBackgroundReady] = useState(true);
|
const [isBackgroundReady, setIsBackgroundReady] = useState(true);
|
||||||
|
// Track when transition video has completed but we're waiting for background to load
|
||||||
const [pendingTransitionComplete, setPendingTransitionComplete] =
|
const [pendingTransitionComplete, setPendingTransitionComplete] =
|
||||||
useState(false);
|
useState(false);
|
||||||
const [activeGalleryCarousel, setActiveGalleryCarousel] = useState<{
|
const [activeGalleryCarousel, setActiveGalleryCarousel] = useState<{
|
||||||
@ -212,7 +213,9 @@ export default function RuntimePresentation({
|
|||||||
applyPageSelection(targetPageId, isBack ?? false);
|
applyPageSelection(targetPageId, isBack ?? false);
|
||||||
});
|
});
|
||||||
setIsBackgroundReady(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);
|
setPendingTransitionComplete(true);
|
||||||
} else {
|
} else {
|
||||||
// No target page - clean up and remove overlay
|
// No target page - clean up and remove overlay
|
||||||
@ -237,27 +240,17 @@ export default function RuntimePresentation({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Use shared background transition hook for crossfade effects
|
// Use shared background transition hook for crossfade effects
|
||||||
const {
|
// NOTE: fadeOut config is NOT used for video transitions.
|
||||||
isOverlayFadingOut,
|
// Video transitions end instantly (last frame = new page, then overlay removed).
|
||||||
resetFadeOut,
|
// fadeIn is used for non-video navigation (crossfade 500ms).
|
||||||
isFadingIn,
|
const { isFadingIn, onFadeInAnimationEnd, resetFadeIn } =
|
||||||
onFadeInAnimationEnd,
|
useBackgroundTransition({
|
||||||
resetFadeIn,
|
pageSwitch,
|
||||||
} = useBackgroundTransition({
|
// No fadeOut - video transitions don't use fade
|
||||||
pageSwitch,
|
fadeIn: {
|
||||||
fadeOut: {
|
hasActiveTransition: Boolean(transitionPreview),
|
||||||
pendingTransitionComplete,
|
},
|
||||||
isBackgroundReady,
|
});
|
||||||
transitionVideoRef,
|
|
||||||
onTransitionCleanup: useCallback(() => {
|
|
||||||
setTransitionPreview(null);
|
|
||||||
setPendingTransitionComplete(false);
|
|
||||||
}, []),
|
|
||||||
},
|
|
||||||
fadeIn: {
|
|
||||||
hasActiveTransition: Boolean(transitionPreview),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const toggleFullscreen = useCallback(async () => {
|
const toggleFullscreen = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@ -336,6 +329,21 @@ export default function RuntimePresentation({
|
|||||||
}
|
}
|
||||||
}, [selectedPage?.background_image_url, selectedPage?.background_video_url]);
|
}, [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:
|
// Safari Black Flash Prevention:
|
||||||
// Update lastKnownBgUrl when a background is successfully displayed.
|
// Update lastKnownBgUrl when a background is successfully displayed.
|
||||||
// This creates a "snapshot" that persists through transitions.
|
// This creates a "snapshot" that persists through transitions.
|
||||||
@ -367,10 +375,8 @@ export default function RuntimePresentation({
|
|||||||
if (!targetPage) return;
|
if (!targetPage) return;
|
||||||
|
|
||||||
if (transitionVideoUrl) {
|
if (transitionVideoUrl) {
|
||||||
// Reset states from previous transition before starting new one
|
// Reset states from previous transition/navigation
|
||||||
// This prevents the fade-out/fade-in effects from re-triggering
|
|
||||||
resetFadeIn();
|
resetFadeIn();
|
||||||
resetFadeOut();
|
|
||||||
setPendingTransitionComplete(false);
|
setPendingTransitionComplete(false);
|
||||||
// Play transition using useTransitionPlayback hook
|
// Play transition using useTransitionPlayback hook
|
||||||
setTransitionPreview({
|
setTransitionPreview({
|
||||||
@ -397,7 +403,7 @@ export default function RuntimePresentation({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[pages, pageSwitch, resetFadeOut, resetFadeIn, applyPageSelection],
|
[pages, pageSwitch, resetFadeIn, applyPageSelection],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleElementClick = useCallback(
|
const handleElementClick = useCallback(
|
||||||
@ -722,14 +728,15 @@ export default function RuntimePresentation({
|
|||||||
|
|
||||||
{/* Transition overlay - uses useTransitionPlayback hook for smooth transitions */}
|
{/* Transition overlay - uses useTransitionPlayback hook for smooth transitions */}
|
||||||
{/* Opacity is 0 during 'preparing' phase to show old page while video loads */}
|
{/* 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 && (
|
{transitionPreview && (
|
||||||
<TransitionPreviewOverlay
|
<TransitionPreviewOverlay
|
||||||
videoRef={transitionVideoRef}
|
videoRef={transitionVideoRef}
|
||||||
isActive={true}
|
isActive={true}
|
||||||
isBuffering={transitionPhase === 'preparing' || isBuffering}
|
isBuffering={transitionPhase === 'preparing' || isBuffering}
|
||||||
letterboxStyles={letterboxStyles}
|
letterboxStyles={letterboxStyles}
|
||||||
opacity={isOverlayFadingOut ? 0 : 1}
|
opacity={1}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -43,18 +43,13 @@ export const CANVAS_CONFIG = {
|
|||||||
|
|
||||||
// Page transition effects
|
// Page transition effects
|
||||||
pageTransition: {
|
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.
|
* 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,
|
easing: 'ease-out' as const,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -12,6 +12,11 @@
|
|||||||
@import "_theme.css";
|
@import "_theme.css";
|
||||||
@import '_rich-text.css';
|
@import '_rich-text.css';
|
||||||
|
|
||||||
|
/* Page transition timing - single source of truth */
|
||||||
|
:root {
|
||||||
|
--crossfade-duration: 700ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.introjs-tooltip {
|
.introjs-tooltip {
|
||||||
@apply min-w-[400px] max-w-[480px] p-2 !important;
|
@apply min-w-[400px] max-w-[480px] p-2 !important;
|
||||||
@ -84,13 +89,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Crossfade animation classes - GPU accelerated for Safari */
|
/* Crossfade animation classes - GPU accelerated for Safari */
|
||||||
|
/* Duration controlled by --crossfade-duration CSS variable (single source of truth) */
|
||||||
.animate-crossfade-in {
|
.animate-crossfade-in {
|
||||||
/* Explicit initial state prevents Safari flash during animation setup */
|
/* Explicit initial state prevents Safari flash during animation setup */
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
-webkit-transform: translate3d(0, 0, 0);
|
-webkit-transform: translate3d(0, 0, 0);
|
||||||
transform: translate3d(0, 0, 0);
|
transform: translate3d(0, 0, 0);
|
||||||
-webkit-animation: page-crossfade-in 300ms ease-out forwards;
|
-webkit-animation: page-crossfade-in var(--crossfade-duration, 700ms) ease-out forwards;
|
||||||
animation: page-crossfade-in 300ms ease-out forwards;
|
animation: page-crossfade-in var(--crossfade-duration, 700ms) ease-out forwards;
|
||||||
-webkit-backface-visibility: hidden;
|
-webkit-backface-visibility: hidden;
|
||||||
backface-visibility: hidden;
|
backface-visibility: hidden;
|
||||||
/* Only animate opacity - transform is for GPU layer promotion */
|
/* Only animate opacity - transform is for GPU layer promotion */
|
||||||
@ -102,8 +108,8 @@
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
-webkit-transform: translate3d(0, 0, 0);
|
-webkit-transform: translate3d(0, 0, 0);
|
||||||
transform: translate3d(0, 0, 0);
|
transform: translate3d(0, 0, 0);
|
||||||
-webkit-animation: page-crossfade-out 300ms ease-out forwards;
|
-webkit-animation: page-crossfade-out var(--crossfade-duration, 700ms) ease-out forwards;
|
||||||
animation: page-crossfade-out 300ms ease-out forwards;
|
animation: page-crossfade-out var(--crossfade-duration, 700ms) ease-out forwards;
|
||||||
-webkit-backface-visibility: hidden;
|
-webkit-backface-visibility: hidden;
|
||||||
backface-visibility: hidden;
|
backface-visibility: hidden;
|
||||||
/* Only animate opacity - transform is for GPU layer promotion */
|
/* Only animate opacity - transform is for GPU layer promotion */
|
||||||
|
|||||||
@ -2,16 +2,18 @@
|
|||||||
* useBackgroundTransition Hook
|
* useBackgroundTransition Hook
|
||||||
*
|
*
|
||||||
* Manages background transition effects when switching between pages.
|
* Manages background transition effects when switching between pages.
|
||||||
* Handles the fade-out animation of the transition video overlay,
|
* Handles crossfade animation for non-video navigation and
|
||||||
* fade-in animation for non-transition navigation, and
|
|
||||||
* coordinates with the page switch hook to clear previous backgrounds.
|
* coordinates with the page switch hook to clear previous backgrounds.
|
||||||
*
|
*
|
||||||
* This hook consolidates the background transition logic used by both
|
* This hook consolidates the background transition logic used by both
|
||||||
* RuntimePresentation and constructor.tsx.
|
* 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:
|
* Two modes:
|
||||||
* 1. Full mode (RuntimePresentation): Fade-out animation + fade-in + direct navigation clearing
|
* 1. fadeIn mode: Crossfade animation for direct (non-video) page navigation
|
||||||
* 2. Simple mode (constructor): Direct navigation clearing + optional fade-in
|
* 2. fadeOut mode: Legacy support - kept for backwards compatibility but not recommended
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -21,13 +23,11 @@ import {
|
|||||||
useCallback,
|
useCallback,
|
||||||
useRef,
|
useRef,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { CANVAS_CONFIG } from '../config/canvas.config';
|
import {
|
||||||
import { isSafari, scheduleAfterPaintSafari } from '../lib/browserUtils';
|
isSafari,
|
||||||
|
scheduleAfterPaintSafari,
|
||||||
/**
|
getCrossfadeDuration,
|
||||||
* Fade-out duration from config (for transition video overlay)
|
} from '../lib/browserUtils';
|
||||||
*/
|
|
||||||
const FADE_OUT_DURATION_MS = CANVAS_CONFIG.pageTransition.fadeOutDurationMs;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fade-out configuration (optional - for RuntimePresentation)
|
* Fade-out configuration (optional - for RuntimePresentation)
|
||||||
@ -194,6 +194,7 @@ export function useBackgroundTransition({
|
|||||||
const { transitionVideoRef, onTransitionCleanup } = fadeOut;
|
const { transitionVideoRef, onTransitionCleanup } = fadeOut;
|
||||||
|
|
||||||
// After fade completes, remove the overlay
|
// After fade completes, remove the overlay
|
||||||
|
// Duration is read from CSS variable for consistency
|
||||||
const fadeTimer = setTimeout(() => {
|
const fadeTimer = setTimeout(() => {
|
||||||
const video = transitionVideoRef.current;
|
const video = transitionVideoRef.current;
|
||||||
if (video) {
|
if (video) {
|
||||||
@ -209,7 +210,7 @@ export function useBackgroundTransition({
|
|||||||
|
|
||||||
// Reset fade-out state
|
// Reset fade-out state
|
||||||
setIsOverlayFadingOut(false);
|
setIsOverlayFadingOut(false);
|
||||||
}, FADE_OUT_DURATION_MS);
|
}, getCrossfadeDuration());
|
||||||
|
|
||||||
return () => clearTimeout(fadeTimer);
|
return () => clearTimeout(fadeTimer);
|
||||||
}, [fadeOut, isOverlayFadingOut, pageSwitch]);
|
}, [fadeOut, isOverlayFadingOut, pageSwitch]);
|
||||||
|
|||||||
@ -166,6 +166,30 @@ export const scheduleAfterPaint = (callback: () => void): void => {
|
|||||||
}, 0);
|
}, 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.
|
* Schedule a callback with explicit frame delay.
|
||||||
* Useful when you need to ensure a specific number of frames have passed.
|
* Useful when you need to ensure a specific number of frames have passed.
|
||||||
|
|||||||
@ -219,6 +219,10 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
element: CanvasElement;
|
element: CanvasElement;
|
||||||
initialIndex: number;
|
initialIndex: number;
|
||||||
} | null>(null);
|
} | 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 isConstructorEditMode = constructorInteractionMode === 'edit';
|
||||||
const allowedNavigationTypes = useMemo<NavigationElementType[]>(() => {
|
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
|
// Helper to switch pages without flash
|
||||||
// Uses usePageSwitch hook to resolve blob URLs from preload cache
|
// Uses usePageSwitch hook to resolve blob URLs from preload cache
|
||||||
// Also updates storage path state for editing/saving purposes
|
// Also updates storage path state for editing/saving purposes
|
||||||
@ -430,17 +464,15 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
clearSelection();
|
clearSelection();
|
||||||
setSelectedMenuItem('none');
|
setSelectedMenuItem('none');
|
||||||
setErrorMessage('');
|
setErrorMessage('');
|
||||||
requestAnimationFrame(() => {
|
setIsBackgroundReady(false);
|
||||||
requestAnimationFrame(() => {
|
// Signal that transition video completed - wait for background to load
|
||||||
video?.removeAttribute('src');
|
// Overlay will be removed instantly when isBackgroundReady becomes true
|
||||||
video?.load();
|
setPendingTransitionComplete(true);
|
||||||
closeTransitionPreview();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
video?.removeAttribute('src');
|
video?.removeAttribute('src');
|
||||||
video?.load();
|
video?.load();
|
||||||
closeTransitionPreview();
|
closeTransitionPreview();
|
||||||
|
setPendingTransitionComplete(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
timeouts: {
|
timeouts: {
|
||||||
@ -1459,7 +1491,10 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
isSwitching={pageSwitch.isSwitching}
|
isSwitching={pageSwitch.isSwitching}
|
||||||
isNewBgReady={pageSwitch.isNewBgReady}
|
isNewBgReady={pageSwitch.isNewBgReady}
|
||||||
isFadingIn={isFadingIn}
|
isFadingIn={isFadingIn}
|
||||||
onBackgroundReady={() => pageSwitch.markBackgroundReady()}
|
onBackgroundReady={() => {
|
||||||
|
pageSwitch.markBackgroundReady();
|
||||||
|
setIsBackgroundReady(true);
|
||||||
|
}}
|
||||||
videoAutoplay={backgroundVideoAutoplay}
|
videoAutoplay={backgroundVideoAutoplay}
|
||||||
videoLoop={backgroundVideoLoop}
|
videoLoop={backgroundVideoLoop}
|
||||||
videoMuted={backgroundVideoMuted}
|
videoMuted={backgroundVideoMuted}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user