fixed adaptivity issue for transitions, added fades for navigation between pages without transitions
This commit is contained in:
parent
ad9c788b21
commit
d55453a42d
2
frontend/next-env.d.ts
vendored
2
frontend/next-env.d.ts
vendored
@ -1,6 +1,6 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
/// <reference path="./.next/types/routes.d.ts" />
|
/// <reference path="./build/types/routes.d.ts" />
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
|
||||||
|
|||||||
@ -15,8 +15,10 @@ interface CanvasBackgroundProps {
|
|||||||
backgroundVideoUrl?: string;
|
backgroundVideoUrl?: string;
|
||||||
backgroundAudioUrl?: string;
|
backgroundAudioUrl?: string;
|
||||||
previousBgImageUrl?: string;
|
previousBgImageUrl?: string;
|
||||||
|
previousBgVideoUrl?: string;
|
||||||
isSwitching?: boolean;
|
isSwitching?: boolean;
|
||||||
isNewBgReady?: boolean;
|
isNewBgReady?: boolean;
|
||||||
|
isFadingIn?: boolean;
|
||||||
onBackgroundReady?: () => void;
|
onBackgroundReady?: () => void;
|
||||||
// Video playback settings
|
// Video playback settings
|
||||||
videoAutoplay?: boolean;
|
videoAutoplay?: boolean;
|
||||||
@ -31,8 +33,10 @@ const CanvasBackground: React.FC<CanvasBackgroundProps> = ({
|
|||||||
backgroundVideoUrl,
|
backgroundVideoUrl,
|
||||||
backgroundAudioUrl,
|
backgroundAudioUrl,
|
||||||
previousBgImageUrl,
|
previousBgImageUrl,
|
||||||
|
previousBgVideoUrl,
|
||||||
isSwitching = false,
|
isSwitching = false,
|
||||||
isNewBgReady = false,
|
isNewBgReady = false,
|
||||||
|
isFadingIn = false,
|
||||||
onBackgroundReady,
|
onBackgroundReady,
|
||||||
videoAutoplay = true,
|
videoAutoplay = true,
|
||||||
videoLoop = true,
|
videoLoop = true,
|
||||||
@ -94,10 +98,12 @@ const CanvasBackground: React.FC<CanvasBackgroundProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Previous background overlay - shows during page switch until new bg is ready */}
|
{/* Previous background overlays - show during loading AND crossfade.
|
||||||
{previousBgImageUrl && isSwitching && !isNewBgReady && (
|
Uses CSS animation for fade-out effect during crossfade.
|
||||||
|
z-0 keeps them BELOW new backgrounds (z-1). */}
|
||||||
|
{previousBgImageUrl && (isFadingIn || (isSwitching && !isNewBgReady)) && (
|
||||||
<div
|
<div
|
||||||
className='pointer-events-none absolute inset-0 z-10'
|
className={`pointer-events-none absolute inset-0 z-0 ${isFadingIn ? 'animate-crossfade-out' : ''}`}
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: `url("${previousBgImageUrl}")`,
|
backgroundImage: `url("${previousBgImageUrl}")`,
|
||||||
backgroundSize: 'contain',
|
backgroundSize: 'contain',
|
||||||
@ -106,6 +112,16 @@ const CanvasBackground: React.FC<CanvasBackgroundProps> = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{previousBgVideoUrl && (isFadingIn || (isSwitching && !isNewBgReady)) && (
|
||||||
|
<video
|
||||||
|
className={`absolute inset-0 z-0 h-full w-full object-contain pointer-events-none ${isFadingIn ? 'animate-crossfade-out' : ''}`}
|
||||||
|
src={previousBgVideoUrl}
|
||||||
|
autoPlay
|
||||||
|
loop
|
||||||
|
muted
|
||||||
|
playsInline
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Background video - z-1 keeps it below backdrop blur layer (z-5) */}
|
{/* Background video - z-1 keeps it below backdrop blur layer (z-5) */}
|
||||||
{backgroundVideoUrl && (
|
{backgroundVideoUrl && (
|
||||||
|
|||||||
@ -4,6 +4,9 @@
|
|||||||
* Full-screen overlay for transition video preview.
|
* Full-screen overlay for transition video preview.
|
||||||
* Designed to work with useTransitionPlayback hook which manages
|
* Designed to work with useTransitionPlayback hook which manages
|
||||||
* video src and playback externally via the videoRef.
|
* video src and playback externally via the videoRef.
|
||||||
|
*
|
||||||
|
* Supports letterbox mode to constrain transitions within canvas bounds,
|
||||||
|
* matching the behavior of background images and UI elements.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
@ -15,26 +18,53 @@ interface TransitionPreviewOverlayProps {
|
|||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
/** Whether the video is currently buffering (used to hide video during load) */
|
/** Whether the video is currently buffering (used to hide video during load) */
|
||||||
isBuffering?: boolean;
|
isBuffering?: boolean;
|
||||||
|
/** Letterbox styles from useCanvasScale - positions overlay within canvas bounds */
|
||||||
|
letterboxStyles?: React.CSSProperties;
|
||||||
|
/** Video object-fit mode (default: 'contain' to match backgrounds) */
|
||||||
|
videoFit?: 'contain' | 'cover';
|
||||||
|
/** Additional opacity value for fade-out effects (0-1) */
|
||||||
|
opacity?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TransitionPreviewOverlay: React.FC<TransitionPreviewOverlayProps> = ({
|
const TransitionPreviewOverlay: React.FC<TransitionPreviewOverlayProps> = ({
|
||||||
videoRef,
|
videoRef,
|
||||||
isActive,
|
isActive,
|
||||||
isBuffering = false,
|
isBuffering = false,
|
||||||
|
letterboxStyles,
|
||||||
|
videoFit = 'contain',
|
||||||
|
opacity,
|
||||||
}) => {
|
}) => {
|
||||||
if (!isActive) return null;
|
if (!isActive) return null;
|
||||||
|
|
||||||
|
// Video opacity: 0 while buffering, 1 otherwise
|
||||||
|
const videoOpacity = isBuffering ? 0 : 1;
|
||||||
|
// Container opacity: controlled by parent for fade-out effects
|
||||||
|
const containerOpacity = opacity ?? 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='fixed inset-0 z-50 overflow-hidden pointer-events-none'>
|
// Outer: full viewport with black background (letterbox bars)
|
||||||
<video
|
// No fade transition - transition video itself is the effect
|
||||||
ref={videoRef}
|
<div
|
||||||
className='absolute inset-0 h-full w-full object-cover transition-opacity duration-300 ease-linear'
|
className='fixed inset-0 z-50 overflow-hidden pointer-events-none bg-black'
|
||||||
style={{ opacity: isBuffering ? 0 : 1 }}
|
style={{ opacity: containerOpacity }}
|
||||||
muted
|
>
|
||||||
playsInline
|
{/* Inner: respects letterbox dimensions when provided */}
|
||||||
preload='auto'
|
<div
|
||||||
disablePictureInPicture
|
className='overflow-hidden'
|
||||||
/>
|
style={letterboxStyles || { position: 'absolute', inset: 0 }}
|
||||||
|
>
|
||||||
|
<video
|
||||||
|
ref={videoRef}
|
||||||
|
className={`absolute inset-0 h-full w-full transition-opacity duration-300 ease-linear ${
|
||||||
|
videoFit === 'cover' ? 'object-cover' : 'object-contain'
|
||||||
|
}`}
|
||||||
|
style={{ opacity: videoOpacity }}
|
||||||
|
muted
|
||||||
|
playsInline
|
||||||
|
preload='auto'
|
||||||
|
disablePictureInPicture
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import BaseButton from './BaseButton';
|
|||||||
import CardBox from './CardBox';
|
import CardBox from './CardBox';
|
||||||
import { OfflineToggle } from './Offline/OfflineToggle';
|
import { OfflineToggle } from './Offline/OfflineToggle';
|
||||||
import RuntimeElement from './RuntimeElement';
|
import RuntimeElement from './RuntimeElement';
|
||||||
|
import TransitionPreviewOverlay from './Constructor/TransitionPreviewOverlay';
|
||||||
import GalleryCarouselOverlay from './UiElements/GalleryCarouselOverlay';
|
import GalleryCarouselOverlay from './UiElements/GalleryCarouselOverlay';
|
||||||
import { BackdropPortalProvider } from './BackdropPortal';
|
import { BackdropPortalProvider } from './BackdropPortal';
|
||||||
import { RotatePrompt } from './RotatePrompt';
|
import { RotatePrompt } from './RotatePrompt';
|
||||||
@ -226,12 +227,12 @@ export default function RuntimePresentation({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Use shared background transition hook for fade-out and fade-in effects
|
// Use shared background transition hook for crossfade effects
|
||||||
const {
|
const {
|
||||||
isOverlayFadingOut,
|
isOverlayFadingOut,
|
||||||
resetFadeOut,
|
resetFadeOut,
|
||||||
isFadingIn,
|
isFadingIn,
|
||||||
elementsOpacity,
|
onFadeInAnimationEnd,
|
||||||
resetFadeIn,
|
resetFadeIn,
|
||||||
} = useBackgroundTransition({
|
} = useBackgroundTransition({
|
||||||
pageSwitch,
|
pageSwitch,
|
||||||
@ -349,10 +350,10 @@ export default function RuntimePresentation({
|
|||||||
isReverse: isBack,
|
isReverse: isBack,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Direct navigation - use shared hook for smooth transition
|
// Direct navigation with crossfade effect:
|
||||||
// Reset fade-in state to start fresh
|
// useBackgroundTransition detects switching and applies animation classes
|
||||||
resetFadeIn();
|
// - New page gets animate-crossfade-in (0 → 1)
|
||||||
// Previous background stays visible until new one is ready
|
// - Previous background gets animate-crossfade-out (1 → 0)
|
||||||
setIsBackgroundReady(false);
|
setIsBackgroundReady(false);
|
||||||
// Mark this page as initialized to prevent redundant effect calls
|
// Mark this page as initialized to prevent redundant effect calls
|
||||||
lastInitializedPageIdRef.current = targetPageId;
|
lastInitializedPageIdRef.current = targetPageId;
|
||||||
@ -534,65 +535,17 @@ export default function RuntimePresentation({
|
|||||||
style={{
|
style={{
|
||||||
...cssVars,
|
...cssVars,
|
||||||
...letterboxStyles,
|
...letterboxStyles,
|
||||||
backgroundImage: backgroundImageUrl
|
|
||||||
? `url("${backgroundImageUrl}")`
|
|
||||||
: undefined,
|
|
||||||
backgroundSize: 'contain',
|
|
||||||
backgroundPosition: 'center',
|
|
||||||
backgroundRepeat: 'no-repeat',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<BackdropPortalProvider>
|
<BackdropPortalProvider>
|
||||||
{/* Background image element - z-1 keeps it below backdrop blur (z-5).
|
{/* Previous background overlays - show during loading AND crossfade.
|
||||||
CSS backgroundImage provides instant display.
|
Uses CSS animation for fade-out effect.
|
||||||
Use native img for blob URLs to prevent repeated fetch requests from Next.js Image. */}
|
Cleared by useBackgroundTransition after fade completes. */}
|
||||||
{backgroundImageUrl && !backgroundVideoUrl && (
|
|
||||||
<div className='absolute inset-0 z-1 pointer-events-none'>
|
|
||||||
{backgroundImageUrl.startsWith('blob:') ? (
|
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
|
||||||
<img
|
|
||||||
key={backgroundImageUrl}
|
|
||||||
src={backgroundImageUrl}
|
|
||||||
alt=''
|
|
||||||
className='absolute inset-0 w-full h-full object-contain'
|
|
||||||
onLoad={() => {
|
|
||||||
setIsBackgroundReady(true);
|
|
||||||
pageSwitch.markBackgroundReady();
|
|
||||||
}}
|
|
||||||
onError={() => {
|
|
||||||
setIsBackgroundReady(true);
|
|
||||||
pageSwitch.markBackgroundReady();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Image
|
|
||||||
key={backgroundImageUrl}
|
|
||||||
src={backgroundImageUrl}
|
|
||||||
alt=''
|
|
||||||
fill
|
|
||||||
sizes='100vw'
|
|
||||||
className='object-contain'
|
|
||||||
priority
|
|
||||||
unoptimized
|
|
||||||
onLoad={() => {
|
|
||||||
setIsBackgroundReady(true);
|
|
||||||
pageSwitch.markBackgroundReady();
|
|
||||||
}}
|
|
||||||
onError={() => {
|
|
||||||
setIsBackgroundReady(true);
|
|
||||||
pageSwitch.markBackgroundReady();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Previous background overlay - shows during direct navigation until new bg is ready */}
|
|
||||||
{pageSwitch.previousBgImageUrl &&
|
{pageSwitch.previousBgImageUrl &&
|
||||||
pageSwitch.isSwitching &&
|
(isFadingIn ||
|
||||||
!pageSwitch.isNewBgReady && (
|
(pageSwitch.isSwitching && !pageSwitch.isNewBgReady)) && (
|
||||||
<div
|
<div
|
||||||
className='absolute inset-0 pointer-events-none z-10'
|
className={`absolute inset-0 pointer-events-none z-0 ${isFadingIn ? 'animate-crossfade-out' : ''}`}
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: `url("${pageSwitch.previousBgImageUrl}")`,
|
backgroundImage: `url("${pageSwitch.previousBgImageUrl}")`,
|
||||||
backgroundSize: 'contain',
|
backgroundSize: 'contain',
|
||||||
@ -601,43 +554,101 @@ export default function RuntimePresentation({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{pageSwitch.previousBgVideoUrl &&
|
||||||
{/* Background video - z-1 keeps it below backdrop blur (z-5) */}
|
(isFadingIn ||
|
||||||
{backgroundVideoUrl && (
|
(pageSwitch.isSwitching && !pageSwitch.isNewBgReady)) && (
|
||||||
<video
|
<video
|
||||||
ref={bgVideoRef}
|
className={`absolute inset-0 h-full w-full object-contain pointer-events-none z-0 ${isFadingIn ? 'animate-crossfade-out' : ''}`}
|
||||||
key={backgroundVideoUrl}
|
src={pageSwitch.previousBgVideoUrl}
|
||||||
className='absolute inset-0 z-1 h-full w-full object-contain'
|
autoPlay
|
||||||
src={backgroundVideoUrl}
|
loop
|
||||||
autoPlay={videoAutoplay}
|
muted
|
||||||
loop={useNativeLoop}
|
playsInline
|
||||||
muted={videoMuted}
|
|
||||||
playsInline
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Page elements - z-40 ensures they appear above carousel background (z-10) and carousel controls (z-30) */}
|
|
||||||
<div
|
|
||||||
className='absolute inset-0 z-40'
|
|
||||||
style={{
|
|
||||||
opacity: elementsOpacity,
|
|
||||||
transition: isFadingIn
|
|
||||||
? `opacity ${CANVAS_CONFIG.pageTransition.fadeInDurationMs}ms ${CANVAS_CONFIG.pageTransition.easing}`
|
|
||||||
: 'none',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{pageElements.map((element: CanvasElement) => (
|
|
||||||
<RuntimeElement
|
|
||||||
key={element.id}
|
|
||||||
element={element}
|
|
||||||
onClick={() => handleElementClick(element)}
|
|
||||||
resolveUrl={resolveUrlWithBlob}
|
|
||||||
onGalleryCardClick={(cardIndex) =>
|
|
||||||
handleGalleryCardClick(element, cardIndex)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
))}
|
)}
|
||||||
|
|
||||||
|
{/* New page content wrapper - fades in for non-transition navigation.
|
||||||
|
z-1 ensures it's above previous backgrounds (z-0) during fade.
|
||||||
|
onAnimationEnd resets isFadingIn when CSS animation completes. */}
|
||||||
|
<div
|
||||||
|
data-testid='page-content-wrapper'
|
||||||
|
className={`absolute inset-0 z-1 ${isFadingIn ? 'animate-crossfade-in' : ''}`}
|
||||||
|
onAnimationEnd={onFadeInAnimationEnd}
|
||||||
|
>
|
||||||
|
{/* Background image element - z-1 keeps it below backdrop blur (z-5).
|
||||||
|
CSS backgroundImage provides instant display.
|
||||||
|
Use native img for blob URLs to prevent repeated fetch requests from Next.js Image. */}
|
||||||
|
{backgroundImageUrl && !backgroundVideoUrl && (
|
||||||
|
<div className='absolute inset-0 z-1 pointer-events-none'>
|
||||||
|
{backgroundImageUrl.startsWith('blob:') ? (
|
||||||
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
|
<img
|
||||||
|
key={backgroundImageUrl}
|
||||||
|
src={backgroundImageUrl}
|
||||||
|
alt=''
|
||||||
|
className='absolute inset-0 w-full h-full object-contain'
|
||||||
|
onLoad={() => {
|
||||||
|
setIsBackgroundReady(true);
|
||||||
|
pageSwitch.markBackgroundReady();
|
||||||
|
}}
|
||||||
|
onError={() => {
|
||||||
|
setIsBackgroundReady(true);
|
||||||
|
pageSwitch.markBackgroundReady();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Image
|
||||||
|
key={backgroundImageUrl}
|
||||||
|
src={backgroundImageUrl}
|
||||||
|
alt=''
|
||||||
|
fill
|
||||||
|
sizes='100vw'
|
||||||
|
className='object-contain'
|
||||||
|
priority
|
||||||
|
unoptimized
|
||||||
|
onLoad={() => {
|
||||||
|
setIsBackgroundReady(true);
|
||||||
|
pageSwitch.markBackgroundReady();
|
||||||
|
}}
|
||||||
|
onError={() => {
|
||||||
|
setIsBackgroundReady(true);
|
||||||
|
pageSwitch.markBackgroundReady();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Background video - z-1 keeps it below backdrop blur (z-5) */}
|
||||||
|
{backgroundVideoUrl && (
|
||||||
|
<video
|
||||||
|
ref={bgVideoRef}
|
||||||
|
key={backgroundVideoUrl}
|
||||||
|
className='absolute inset-0 z-1 h-full w-full object-contain'
|
||||||
|
src={backgroundVideoUrl}
|
||||||
|
autoPlay={videoAutoplay}
|
||||||
|
loop={useNativeLoop}
|
||||||
|
muted={videoMuted}
|
||||||
|
playsInline
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Page elements - z-40 ensures they appear above carousel background (z-10) and carousel controls (z-30) */}
|
||||||
|
<div className='absolute inset-0 z-40'>
|
||||||
|
{pageElements.map((element: CanvasElement) => (
|
||||||
|
<RuntimeElement
|
||||||
|
key={element.id}
|
||||||
|
element={element}
|
||||||
|
onClick={() => handleElementClick(element)}
|
||||||
|
resolveUrl={resolveUrlWithBlob}
|
||||||
|
onGalleryCardClick={(cardIndex) =>
|
||||||
|
handleGalleryCardClick(element, cardIndex)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/* End new page content wrapper */}
|
||||||
|
|
||||||
{/* Controls: Offline toggle and Fullscreen button */}
|
{/* Controls: Offline toggle and Fullscreen button */}
|
||||||
<div className='absolute top-4 right-4 z-50 flex items-center gap-2'>
|
<div className='absolute top-4 right-4 z-50 flex items-center gap-2'>
|
||||||
@ -661,24 +672,13 @@ export default function RuntimePresentation({
|
|||||||
{/* 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 */}
|
{/* Also fades to 0 when isOverlayFadingOut to reveal the new page underneath */}
|
||||||
{transitionPreview && (
|
{transitionPreview && (
|
||||||
<div className='fixed inset-0 z-50 overflow-hidden pointer-events-none'>
|
<TransitionPreviewOverlay
|
||||||
<video
|
videoRef={transitionVideoRef}
|
||||||
ref={transitionVideoRef}
|
isActive={true}
|
||||||
className='absolute inset-0 h-full w-full object-cover transition-opacity duration-300 ease-linear'
|
isBuffering={transitionPhase === 'preparing' || isBuffering}
|
||||||
style={{
|
letterboxStyles={letterboxStyles}
|
||||||
opacity:
|
opacity={isOverlayFadingOut ? 0 : 1}
|
||||||
transitionPhase === 'preparing' ||
|
/>
|
||||||
isBuffering ||
|
|
||||||
isOverlayFadingOut
|
|
||||||
? 0
|
|
||||||
: 1,
|
|
||||||
}}
|
|
||||||
muted
|
|
||||||
playsInline
|
|
||||||
preload='auto'
|
|
||||||
disablePictureInPicture
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Gallery Carousel Overlay */}
|
{/* Gallery Carousel Overlay */}
|
||||||
|
|||||||
@ -43,14 +43,10 @@ export const CANVAS_CONFIG = {
|
|||||||
|
|
||||||
// Page transition effects
|
// Page transition effects
|
||||||
pageTransition: {
|
pageTransition: {
|
||||||
/**
|
|
||||||
* Fade-in duration for non-transition navigation (ms).
|
|
||||||
* Applied when switching pages without a transition video.
|
|
||||||
*/
|
|
||||||
fadeInDurationMs: 500,
|
|
||||||
/**
|
/**
|
||||||
* Fade-out duration for transition video overlay (ms).
|
* Fade-out duration for transition video overlay (ms).
|
||||||
* Applied after transition video finishes playing.
|
* Applied after transition video finishes playing.
|
||||||
|
* Note: Crossfade duration is controlled by CSS in main.css (.animate-crossfade-in/out)
|
||||||
*/
|
*/
|
||||||
fadeOutDurationMs: 300,
|
fadeOutDurationMs: 300,
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -34,6 +34,34 @@
|
|||||||
@apply bg-transparent border border-blue-600 text-blue-600 !important;
|
@apply bg-transparent border border-blue-600 text-blue-600 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Page crossfade animation keyframes */
|
||||||
|
@keyframes page-crossfade-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes page-crossfade-out {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Crossfade animation classes */
|
||||||
|
.animate-crossfade-in {
|
||||||
|
animation: page-crossfade-in 500ms ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-crossfade-out {
|
||||||
|
animation: page-crossfade-out 500ms ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
/* Element appear animation keyframes */
|
/* Element appear animation keyframes */
|
||||||
@keyframes element-fade-in {
|
@keyframes element-fade-in {
|
||||||
from {
|
from {
|
||||||
|
|||||||
@ -14,19 +14,20 @@
|
|||||||
* 2. Simple mode (constructor): Direct navigation clearing + optional fade-in
|
* 2. Simple mode (constructor): Direct navigation clearing + optional fade-in
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useEffect, useState, useCallback, useRef } from 'react';
|
import {
|
||||||
|
useEffect,
|
||||||
|
useLayoutEffect,
|
||||||
|
useState,
|
||||||
|
useCallback,
|
||||||
|
useRef,
|
||||||
|
} from 'react';
|
||||||
import { CANVAS_CONFIG } from '../config/canvas.config';
|
import { CANVAS_CONFIG } from '../config/canvas.config';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fade-out duration from config
|
* Fade-out duration from config (for transition video overlay)
|
||||||
*/
|
*/
|
||||||
const FADE_OUT_DURATION_MS = CANVAS_CONFIG.pageTransition.fadeOutDurationMs;
|
const FADE_OUT_DURATION_MS = CANVAS_CONFIG.pageTransition.fadeOutDurationMs;
|
||||||
|
|
||||||
/**
|
|
||||||
* Fade-in duration from config
|
|
||||||
*/
|
|
||||||
const FADE_IN_DURATION_MS = CANVAS_CONFIG.pageTransition.fadeInDurationMs;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fade-out configuration (optional - for RuntimePresentation)
|
* Fade-out configuration (optional - for RuntimePresentation)
|
||||||
*/
|
*/
|
||||||
@ -47,8 +48,6 @@ export interface FadeOutConfig {
|
|||||||
export interface FadeInConfig {
|
export interface FadeInConfig {
|
||||||
/** Whether a transition video is currently active (disables fade-in) */
|
/** Whether a transition video is currently active (disables fade-in) */
|
||||||
hasActiveTransition: boolean;
|
hasActiveTransition: boolean;
|
||||||
/** Optional duration override (uses config default if not provided) */
|
|
||||||
durationMs?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UseBackgroundTransitionOptions {
|
export interface UseBackgroundTransitionOptions {
|
||||||
@ -58,6 +57,7 @@ export interface UseBackgroundTransitionOptions {
|
|||||||
isSwitching: boolean;
|
isSwitching: boolean;
|
||||||
isNewBgReady: boolean;
|
isNewBgReady: boolean;
|
||||||
previousBgImageUrl: string;
|
previousBgImageUrl: string;
|
||||||
|
previousBgVideoUrl: string;
|
||||||
};
|
};
|
||||||
/** Optional fade-out configuration (for RuntimePresentation) */
|
/** Optional fade-out configuration (for RuntimePresentation) */
|
||||||
fadeOut?: FadeOutConfig;
|
fadeOut?: FadeOutConfig;
|
||||||
@ -70,11 +70,11 @@ export interface UseBackgroundTransitionResult {
|
|||||||
isOverlayFadingOut: boolean;
|
isOverlayFadingOut: boolean;
|
||||||
/** Reset the fade-out state (call before starting a new transition) */
|
/** Reset the fade-out state (call before starting a new transition) */
|
||||||
resetFadeOut: () => void;
|
resetFadeOut: () => void;
|
||||||
/** Whether page content is currently fading in */
|
/** Whether page content is currently fading (crossfade in progress) */
|
||||||
isFadingIn: boolean;
|
isFadingIn: boolean;
|
||||||
/** Opacity value for elements container (0 during fade, 1 when complete) */
|
/** Handler to call when fade-in animation ends (pass to onAnimationEnd) */
|
||||||
elementsOpacity: number;
|
onFadeInAnimationEnd: () => void;
|
||||||
/** Reset fade-in state before starting new navigation */
|
/** Reset fade-in state (for cleanup or cancellation) */
|
||||||
resetFadeIn: () => void;
|
resetFadeIn: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,7 +83,7 @@ export interface UseBackgroundTransitionResult {
|
|||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* // Full mode with fade-out and fade-in (RuntimePresentation)
|
* // Full mode with fade-out and fade-in (RuntimePresentation)
|
||||||
* const { isOverlayFadingOut, resetFadeOut, isFadingIn, elementsOpacity, resetFadeIn } = useBackgroundTransition({
|
* const { isOverlayFadingOut, resetFadeOut, isFadingIn, onFadeInAnimationEnd } = useBackgroundTransition({
|
||||||
* pageSwitch,
|
* pageSwitch,
|
||||||
* fadeOut: {
|
* fadeOut: {
|
||||||
* pendingTransitionComplete,
|
* pendingTransitionComplete,
|
||||||
@ -99,6 +99,12 @@ export interface UseBackgroundTransitionResult {
|
|||||||
* },
|
* },
|
||||||
* });
|
* });
|
||||||
*
|
*
|
||||||
|
* // In JSX:
|
||||||
|
* <div
|
||||||
|
* className={isFadingIn ? 'animate-crossfade-in' : ''}
|
||||||
|
* onAnimationEnd={onFadeInAnimationEnd}
|
||||||
|
* >
|
||||||
|
*
|
||||||
* @example
|
* @example
|
||||||
* // Simple mode - direct navigation only (constructor)
|
* // Simple mode - direct navigation only (constructor)
|
||||||
* useBackgroundTransition({ pageSwitch });
|
* useBackgroundTransition({ pageSwitch });
|
||||||
@ -109,15 +115,10 @@ export function useBackgroundTransition({
|
|||||||
fadeIn,
|
fadeIn,
|
||||||
}: UseBackgroundTransitionOptions): UseBackgroundTransitionResult {
|
}: UseBackgroundTransitionOptions): UseBackgroundTransitionResult {
|
||||||
const [isOverlayFadingOut, setIsOverlayFadingOut] = useState(false);
|
const [isOverlayFadingOut, setIsOverlayFadingOut] = useState(false);
|
||||||
|
|
||||||
// Fade-in state
|
|
||||||
const [isFadingIn, setIsFadingIn] = useState(false);
|
const [isFadingIn, setIsFadingIn] = useState(false);
|
||||||
const [elementsOpacity, setElementsOpacity] = useState(1);
|
|
||||||
|
|
||||||
// Track previous isSwitching state to detect transition start
|
// Track previous isSwitching state to detect transition start
|
||||||
const wasSwitchingRef = useRef(false);
|
const wasSwitchingRef = useRef(false);
|
||||||
// Track timer for cleanup
|
|
||||||
const fadeInTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset fade-out state before starting a new transition.
|
* Reset fade-out state before starting a new transition.
|
||||||
@ -128,16 +129,18 @@ export function useBackgroundTransition({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset fade-in state before starting new navigation.
|
* Reset fade-in state (for cleanup or cancellation).
|
||||||
* Clears any in-progress fade animation.
|
|
||||||
*/
|
*/
|
||||||
const resetFadeIn = useCallback(() => {
|
const resetFadeIn = useCallback(() => {
|
||||||
if (fadeInTimerRef.current) {
|
|
||||||
clearTimeout(fadeInTimerRef.current);
|
|
||||||
fadeInTimerRef.current = null;
|
|
||||||
}
|
|
||||||
setIsFadingIn(false);
|
setIsFadingIn(false);
|
||||||
setElementsOpacity(1);
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for onAnimationEnd event.
|
||||||
|
* Called when CSS animation completes - CSS is the source of truth for duration.
|
||||||
|
*/
|
||||||
|
const onFadeInAnimationEnd = useCallback(() => {
|
||||||
|
setIsFadingIn(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -187,92 +190,55 @@ export function useBackgroundTransition({
|
|||||||
}, [fadeOut, isOverlayFadingOut, pageSwitch]);
|
}, [fadeOut, isOverlayFadingOut, pageSwitch]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Effect: Clear previous background overlay when new background is ready (direct navigation).
|
* Effect: Clear previous background overlay after fade completes (direct navigation).
|
||||||
*
|
*
|
||||||
* This handles the case when navigating without a transition video.
|
* The previous background stays visible during the entire fade animation,
|
||||||
* The previous background stays visible until the new one is ready to paint.
|
* providing a smooth crossfade effect. Only cleared after fade ends.
|
||||||
*/
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (pageSwitch.isSwitching && pageSwitch.isNewBgReady && !isFadingIn) {
|
||||||
pageSwitch.isSwitching &&
|
// Fade is complete - clear the previous background overlay
|
||||||
pageSwitch.isNewBgReady &&
|
// This also resets isSwitching state so next navigation triggers fade-in
|
||||||
pageSwitch.previousBgImageUrl
|
|
||||||
) {
|
|
||||||
// New background is ready - clear the previous background overlay
|
|
||||||
pageSwitch.clearPreviousBackground();
|
pageSwitch.clearPreviousBackground();
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
pageSwitch.isSwitching,
|
pageSwitch.isSwitching,
|
||||||
pageSwitch.isNewBgReady,
|
pageSwitch.isNewBgReady,
|
||||||
pageSwitch.previousBgImageUrl,
|
|
||||||
pageSwitch.clearPreviousBackground,
|
pageSwitch.clearPreviousBackground,
|
||||||
|
isFadingIn,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Effect: Fade-in page content on non-transition navigation.
|
* Layout effect: Set up crossfade BEFORE browser paints when switching starts.
|
||||||
|
* useLayoutEffect runs synchronously after DOM mutations but before paint,
|
||||||
|
* preventing any flash of new content at full opacity.
|
||||||
*
|
*
|
||||||
* Trigger: pageSwitch.isSwitching becomes true AND no active transition
|
* IMPORTANT: Skip this for transitions - transition video IS the effect.
|
||||||
*
|
|
||||||
* Sequence:
|
|
||||||
* 1. Navigation starts (isSwitching: false → true)
|
|
||||||
* 2. No transition video active
|
|
||||||
* 3. Set elementsOpacity = 0
|
|
||||||
* 4. Use double RAF to ensure paint before animation
|
|
||||||
* 5. Set elementsOpacity = 1 (CSS animates)
|
|
||||||
* 6. After duration, set isFadingIn = false
|
|
||||||
*/
|
*/
|
||||||
useEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!fadeIn) {
|
if (!fadeIn) {
|
||||||
wasSwitchingRef.current = pageSwitch.isSwitching;
|
wasSwitchingRef.current = pageSwitch.isSwitching;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { hasActiveTransition, durationMs = FADE_IN_DURATION_MS } = fadeIn;
|
const { hasActiveTransition } = fadeIn;
|
||||||
const justStartedSwitching =
|
const justStartedSwitching =
|
||||||
pageSwitch.isSwitching && !wasSwitchingRef.current;
|
pageSwitch.isSwitching && !wasSwitchingRef.current;
|
||||||
|
|
||||||
wasSwitchingRef.current = pageSwitch.isSwitching;
|
wasSwitchingRef.current = pageSwitch.isSwitching;
|
||||||
|
|
||||||
// Start fade-in when:
|
// Only start crossfade for NON-transition navigation
|
||||||
// - Just started switching (transition from false to true)
|
// Transitions use video overlay - no fade needed
|
||||||
// - No active transition video
|
|
||||||
if (justStartedSwitching && !hasActiveTransition) {
|
if (justStartedSwitching && !hasActiveTransition) {
|
||||||
setIsFadingIn(true);
|
setIsFadingIn(true);
|
||||||
setElementsOpacity(0);
|
|
||||||
|
|
||||||
// Double RAF ensures opacity:0 is painted before transition starts
|
|
||||||
// (Same pattern as usePageSwitch.ts:396-397)
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
setElementsOpacity(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clear any existing timer
|
|
||||||
if (fadeInTimerRef.current) {
|
|
||||||
clearTimeout(fadeInTimerRef.current);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark fade as complete after duration
|
|
||||||
fadeInTimerRef.current = setTimeout(() => {
|
|
||||||
setIsFadingIn(false);
|
|
||||||
fadeInTimerRef.current = null;
|
|
||||||
}, durationMs);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup on unmount
|
|
||||||
return () => {
|
|
||||||
if (fadeInTimerRef.current) {
|
|
||||||
clearTimeout(fadeInTimerRef.current);
|
|
||||||
fadeInTimerRef.current = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [pageSwitch.isSwitching, fadeIn]);
|
}, [pageSwitch.isSwitching, fadeIn]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isOverlayFadingOut,
|
isOverlayFadingOut,
|
||||||
resetFadeOut,
|
resetFadeOut,
|
||||||
isFadingIn,
|
isFadingIn,
|
||||||
elementsOpacity,
|
onFadeInAnimationEnd,
|
||||||
resetFadeIn,
|
resetFadeIn,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -63,6 +63,8 @@ export interface UsePageSwitchResult {
|
|||||||
currentBgAudioUrl: string;
|
currentBgAudioUrl: string;
|
||||||
/** Previous background image URL (for overlay) */
|
/** Previous background image URL (for overlay) */
|
||||||
previousBgImageUrl: string;
|
previousBgImageUrl: string;
|
||||||
|
/** Previous background video URL (for overlay during fade) */
|
||||||
|
previousBgVideoUrl: string;
|
||||||
/** Whether we're in the middle of a page switch */
|
/** Whether we're in the middle of a page switch */
|
||||||
isSwitching: boolean;
|
isSwitching: boolean;
|
||||||
/** Whether the new background is ready to display */
|
/** Whether the new background is ready to display */
|
||||||
@ -198,6 +200,10 @@ export function usePageSwitch(
|
|||||||
const previousBgImageUrlRef = useRef('');
|
const previousBgImageUrlRef = useRef('');
|
||||||
previousBgImageUrlRef.current = previousBgImageUrl;
|
previousBgImageUrlRef.current = previousBgImageUrl;
|
||||||
|
|
||||||
|
const [previousBgVideoUrl, setPreviousBgVideoUrl] = useState('');
|
||||||
|
const previousBgVideoUrlRef = useRef('');
|
||||||
|
previousBgVideoUrlRef.current = previousBgVideoUrl;
|
||||||
|
|
||||||
// Transition state
|
// Transition state
|
||||||
const [isSwitching, setIsSwitching] = useState(false);
|
const [isSwitching, setIsSwitching] = useState(false);
|
||||||
const [isNewBgReady, setIsNewBgReady] = useState(true);
|
const [isNewBgReady, setIsNewBgReady] = useState(true);
|
||||||
@ -362,16 +368,20 @@ export function usePageSwitch(
|
|||||||
setCurrentBgVideoUrl('');
|
setCurrentBgVideoUrl('');
|
||||||
setCurrentBgAudioUrl('');
|
setCurrentBgAudioUrl('');
|
||||||
setPreviousBgImageUrl('');
|
setPreviousBgImageUrl('');
|
||||||
|
setPreviousBgVideoUrl('');
|
||||||
setIsSwitching(false);
|
setIsSwitching(false);
|
||||||
setIsNewBgReady(true);
|
setIsNewBgReady(true);
|
||||||
onSwitched?.();
|
onSwitched?.();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save current image as previous for overlay (use ref to avoid dependency)
|
// Save current backgrounds as previous for overlay (use refs to avoid dependencies)
|
||||||
if (currentBgImageUrlRef.current) {
|
if (currentBgImageUrlRef.current) {
|
||||||
setPreviousBgImageUrl(currentBgImageUrlRef.current);
|
setPreviousBgImageUrl(currentBgImageUrlRef.current);
|
||||||
}
|
}
|
||||||
|
if (currentBgVideoUrlRef.current) {
|
||||||
|
setPreviousBgVideoUrl(currentBgVideoUrlRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
setIsSwitching(true);
|
setIsSwitching(true);
|
||||||
setIsNewBgReady(false);
|
setIsNewBgReady(false);
|
||||||
@ -433,16 +443,21 @@ export function usePageSwitch(
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear the previous background overlay
|
* Clear the previous background overlay (both image and video)
|
||||||
*/
|
*/
|
||||||
const clearPreviousBackground = useCallback(() => {
|
const clearPreviousBackground = useCallback(() => {
|
||||||
const prevUrl = previousBgImageUrlRef.current;
|
const prevImageUrl = previousBgImageUrlRef.current;
|
||||||
|
const prevVideoUrl = previousBgVideoUrlRef.current;
|
||||||
setPreviousBgImageUrl('');
|
setPreviousBgImageUrl('');
|
||||||
|
setPreviousBgVideoUrl('');
|
||||||
setIsSwitching(false);
|
setIsSwitching(false);
|
||||||
|
|
||||||
// Revoke the previous blob URL after clearing
|
// Revoke the previous blob URLs after clearing
|
||||||
if (prevUrl) {
|
if (prevImageUrl) {
|
||||||
revokeBlobUrl(prevUrl);
|
revokeBlobUrl(prevImageUrl);
|
||||||
|
}
|
||||||
|
if (prevVideoUrl) {
|
||||||
|
revokeBlobUrl(prevVideoUrl);
|
||||||
}
|
}
|
||||||
}, [revokeBlobUrl]);
|
}, [revokeBlobUrl]);
|
||||||
|
|
||||||
@ -451,6 +466,7 @@ export function usePageSwitch(
|
|||||||
currentBgVideoUrl,
|
currentBgVideoUrl,
|
||||||
currentBgAudioUrl,
|
currentBgAudioUrl,
|
||||||
previousBgImageUrl,
|
previousBgImageUrl,
|
||||||
|
previousBgVideoUrl,
|
||||||
isSwitching,
|
isSwitching,
|
||||||
isNewBgReady,
|
isNewBgReady,
|
||||||
switchToPage,
|
switchToPage,
|
||||||
|
|||||||
@ -358,10 +358,9 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
// Destructure stable callback reference to avoid infinite loops in useEffect deps
|
// Destructure stable callback reference to avoid infinite loops in useEffect deps
|
||||||
const pageSwitchToPage = pageSwitch.switchToPage;
|
const pageSwitchToPage = pageSwitch.switchToPage;
|
||||||
|
|
||||||
// Use shared background transition hook for direct navigation clearing and fade-in
|
// Use shared background transition hook for direct navigation clearing and crossfade
|
||||||
// (No fade-out needed in constructor - transitions complete immediately)
|
// Crossfade starts automatically when new background is ready
|
||||||
// NOTE: Must be defined before switchToPage callback which uses resetFadeIn
|
const { isFadingIn } = useBackgroundTransition({
|
||||||
const { isFadingIn, elementsOpacity, resetFadeIn } = useBackgroundTransition({
|
|
||||||
pageSwitch,
|
pageSwitch,
|
||||||
fadeIn: {
|
fadeIn: {
|
||||||
hasActiveTransition: Boolean(transitionPreview),
|
hasActiveTransition: Boolean(transitionPreview),
|
||||||
@ -374,9 +373,6 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
// isBack parameter indicates this is a back navigation (pops history instead of pushing)
|
// isBack parameter indicates this is a back navigation (pops history instead of pushing)
|
||||||
const switchToPage = useCallback(
|
const switchToPage = useCallback(
|
||||||
async (page: TourPage | null, isBack = false) => {
|
async (page: TourPage | null, isBack = false) => {
|
||||||
// Reset fade-in state to start fresh
|
|
||||||
resetFadeIn();
|
|
||||||
|
|
||||||
// Mark this page as initialized to prevent redundant effect calls
|
// Mark this page as initialized to prevent redundant effect calls
|
||||||
if (page) {
|
if (page) {
|
||||||
lastInitializedPageIdRef.current = page.id;
|
lastInitializedPageIdRef.current = page.id;
|
||||||
@ -386,6 +382,7 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
updateBackgroundFromPage(page);
|
updateBackgroundFromPage(page);
|
||||||
|
|
||||||
// Use hook to resolve and set blob URLs for display
|
// Use hook to resolve and set blob URLs for display
|
||||||
|
// Fade starts automatically when new background is ready (crossfade effect)
|
||||||
await pageSwitchToPage(
|
await pageSwitchToPage(
|
||||||
page
|
page
|
||||||
? {
|
? {
|
||||||
@ -403,12 +400,7 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[
|
[pageSwitchToPage, updateBackgroundFromPage, applyPageSelection],
|
||||||
pageSwitchToPage,
|
|
||||||
updateBackgroundFromPage,
|
|
||||||
applyPageSelection,
|
|
||||||
resetFadeIn,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const { isBuffering: isReverseBuffering } = useTransitionPlayback({
|
const { isBuffering: isReverseBuffering } = useTransitionPlayback({
|
||||||
@ -1466,8 +1458,10 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
backgroundVideoUrl={backgroundVideoSrc}
|
backgroundVideoUrl={backgroundVideoSrc}
|
||||||
backgroundAudioUrl={backgroundAudioSrc}
|
backgroundAudioUrl={backgroundAudioSrc}
|
||||||
previousBgImageUrl={pageSwitch.previousBgImageUrl}
|
previousBgImageUrl={pageSwitch.previousBgImageUrl}
|
||||||
|
previousBgVideoUrl={pageSwitch.previousBgVideoUrl}
|
||||||
isSwitching={pageSwitch.isSwitching}
|
isSwitching={pageSwitch.isSwitching}
|
||||||
isNewBgReady={pageSwitch.isNewBgReady}
|
isNewBgReady={pageSwitch.isNewBgReady}
|
||||||
|
isFadingIn={isFadingIn}
|
||||||
onBackgroundReady={() => pageSwitch.markBackgroundReady()}
|
onBackgroundReady={() => pageSwitch.markBackgroundReady()}
|
||||||
videoAutoplay={backgroundVideoAutoplay}
|
videoAutoplay={backgroundVideoAutoplay}
|
||||||
videoLoop={backgroundVideoLoop}
|
videoLoop={backgroundVideoLoop}
|
||||||
@ -1478,13 +1472,7 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
|
|
||||||
{/* Elements container - z-10 ensures they appear above backdrop layer */}
|
{/* Elements container - z-10 ensures they appear above backdrop layer */}
|
||||||
<div
|
<div
|
||||||
className='absolute inset-0 z-10'
|
className={`absolute inset-0 z-10 ${isFadingIn ? 'animate-crossfade-in' : ''}`}
|
||||||
style={{
|
|
||||||
opacity: elementsOpacity,
|
|
||||||
transition: isFadingIn
|
|
||||||
? `opacity ${CANVAS_CONFIG.pageTransition.fadeInDurationMs}ms ${CANVAS_CONFIG.pageTransition.easing}`
|
|
||||||
: 'none',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className='absolute inset-0 flex items-center justify-center'>
|
<div className='absolute inset-0 flex items-center justify-center'>
|
||||||
@ -1590,6 +1578,7 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
videoRef={transitionVideoRef}
|
videoRef={transitionVideoRef}
|
||||||
isActive={Boolean(transitionPreview)}
|
isActive={Boolean(transitionPreview)}
|
||||||
isBuffering={isReverseBuffering}
|
isBuffering={isReverseBuffering}
|
||||||
|
letterboxStyles={letterboxStyles}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Gallery Carousel Overlay */}
|
{/* Gallery Carousel Overlay */}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user