205 lines
5.9 KiB
TypeScript
205 lines
5.9 KiB
TypeScript
/**
|
|
* useBackgroundTransition Hook
|
|
*
|
|
* Manages background transition effects when switching between pages.
|
|
* Controls the fade-from-black overlay for smooth page transitions.
|
|
*
|
|
* When a page switch occurs:
|
|
* 1. Page content switches instantly (hidden by black overlay)
|
|
* 2. Black overlay fades out (1 → 0) revealing new page
|
|
*
|
|
* NOTE: Video transitions do NOT use fades - the video itself IS the transition.
|
|
*/
|
|
|
|
import {
|
|
useEffect,
|
|
useLayoutEffect,
|
|
useState,
|
|
useCallback,
|
|
useRef,
|
|
} from 'react';
|
|
import { isSafari, getCrossfadeDuration } from '../lib/browserUtils';
|
|
import type { ResolvedTransitionSettings } from '../types/transition';
|
|
|
|
/**
|
|
* Fade-in configuration for page content
|
|
*/
|
|
export interface FadeInConfig {
|
|
/** Whether a transition video is currently active (disables fade-in) */
|
|
hasActiveTransition: boolean;
|
|
}
|
|
|
|
export interface UseBackgroundTransitionOptions {
|
|
/** Page switch hook instance for clearing previous background */
|
|
pageSwitch: {
|
|
clearPreviousBackground: () => void;
|
|
isSwitching: boolean;
|
|
isNewBgReady: boolean;
|
|
previousBgImageUrl: string;
|
|
previousBgVideoUrl: string;
|
|
};
|
|
/** Optional fade-in configuration for page content */
|
|
fadeIn?: FadeInConfig;
|
|
/** Optional resolved transition settings for dynamic duration/easing */
|
|
transitionSettings?: ResolvedTransitionSettings | null;
|
|
}
|
|
|
|
export interface UseBackgroundTransitionResult {
|
|
/** Whether page content is currently fading (fade-from-black in progress) */
|
|
isFadingIn: boolean;
|
|
/** Reset fade-in state (for cleanup or cancellation) */
|
|
resetFadeIn: () => void;
|
|
/** Inline style for transition duration and easing */
|
|
transitionStyle: React.CSSProperties;
|
|
}
|
|
|
|
/**
|
|
* Hook for managing background transition effects.
|
|
*
|
|
* @example
|
|
* const { isFadingIn, transitionStyle } = useBackgroundTransition({
|
|
* pageSwitch,
|
|
* fadeIn: {
|
|
* hasActiveTransition: Boolean(transitionPreview),
|
|
* },
|
|
* transitionSettings,
|
|
* });
|
|
*
|
|
* // Render black overlay that fades out
|
|
* <TransitionBlackOverlay isFadingIn={isFadingIn} transitionStyle={transitionStyle} />
|
|
*/
|
|
export function useBackgroundTransition({
|
|
pageSwitch,
|
|
fadeIn,
|
|
transitionSettings,
|
|
}: UseBackgroundTransitionOptions): UseBackgroundTransitionResult {
|
|
const [isFadingIn, setIsFadingIn] = useState(false);
|
|
|
|
// Track previous isSwitching state to detect transition start
|
|
const wasSwitchingRef = useRef(false);
|
|
|
|
// Store transitionSettings in ref to avoid stale closures
|
|
const transitionSettingsRef = useRef(transitionSettings);
|
|
transitionSettingsRef.current = transitionSettings;
|
|
|
|
// Timer ref for fade completion
|
|
const fadeInTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
// Track if animation was already completed
|
|
const fadeInCompletedRef = useRef(false);
|
|
|
|
// Track fadeIn config in ref to avoid stale closure issues
|
|
const fadeInRef = useRef(fadeIn);
|
|
fadeInRef.current = fadeIn;
|
|
|
|
/**
|
|
* Reset fade-in state (for cleanup or cancellation).
|
|
*/
|
|
const resetFadeIn = useCallback(() => {
|
|
setIsFadingIn(false);
|
|
fadeInCompletedRef.current = false;
|
|
if (fadeInTimerRef.current) {
|
|
clearTimeout(fadeInTimerRef.current);
|
|
fadeInTimerRef.current = null;
|
|
}
|
|
}, []);
|
|
|
|
/**
|
|
* Complete fade-in animation.
|
|
*/
|
|
const completeFadeIn = useCallback(() => {
|
|
if (fadeInCompletedRef.current) return;
|
|
fadeInCompletedRef.current = true;
|
|
|
|
if (fadeInTimerRef.current) {
|
|
clearTimeout(fadeInTimerRef.current);
|
|
fadeInTimerRef.current = null;
|
|
}
|
|
|
|
setIsFadingIn(false);
|
|
}, []);
|
|
|
|
/**
|
|
* Effect: Clear previous background overlay after fade completes (direct navigation).
|
|
*/
|
|
useEffect(() => {
|
|
const hasActiveTransition = fadeInRef.current?.hasActiveTransition ?? false;
|
|
|
|
// Skip clearing during video transitions
|
|
if (hasActiveTransition) return;
|
|
|
|
if (pageSwitch.isSwitching && pageSwitch.isNewBgReady && !isFadingIn) {
|
|
pageSwitch.clearPreviousBackground();
|
|
}
|
|
}, [
|
|
pageSwitch.isSwitching,
|
|
pageSwitch.isNewBgReady,
|
|
pageSwitch.clearPreviousBackground,
|
|
isFadingIn,
|
|
]);
|
|
|
|
/**
|
|
* Layout effect: Start fade-from-black when switching starts.
|
|
* useLayoutEffect runs before paint, ensuring overlay appears immediately.
|
|
*/
|
|
useLayoutEffect(() => {
|
|
const currentFadeIn = fadeInRef.current;
|
|
|
|
// Skip if fadeIn config was not provided
|
|
if (!currentFadeIn) {
|
|
wasSwitchingRef.current = pageSwitch.isSwitching;
|
|
return;
|
|
}
|
|
|
|
const justStartedSwitching =
|
|
pageSwitch.isSwitching && !wasSwitchingRef.current;
|
|
|
|
wasSwitchingRef.current = pageSwitch.isSwitching;
|
|
|
|
// Only start fade for NON-transition navigation
|
|
if (justStartedSwitching && !currentFadeIn.hasActiveTransition) {
|
|
fadeInCompletedRef.current = false;
|
|
|
|
if (fadeInTimerRef.current) {
|
|
clearTimeout(fadeInTimerRef.current);
|
|
fadeInTimerRef.current = null;
|
|
}
|
|
|
|
setIsFadingIn(true);
|
|
|
|
// Timer to end fade after animation duration
|
|
const duration = getCrossfadeDuration(
|
|
transitionSettingsRef.current?.durationMs,
|
|
);
|
|
const bufferMs = isSafari() ? 100 : 50;
|
|
|
|
fadeInTimerRef.current = setTimeout(() => {
|
|
fadeInTimerRef.current = null;
|
|
completeFadeIn();
|
|
}, duration + bufferMs);
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [pageSwitch.isSwitching, completeFadeIn]);
|
|
|
|
// Cleanup timer on unmount
|
|
useEffect(() => {
|
|
return () => {
|
|
if (fadeInTimerRef.current) {
|
|
clearTimeout(fadeInTimerRef.current);
|
|
fadeInTimerRef.current = null;
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
const transitionStyle: React.CSSProperties = {
|
|
'--transition-duration': `${transitionSettings?.durationMs ?? 700}ms`,
|
|
'--transition-easing': transitionSettings?.easing ?? 'ease-in-out',
|
|
'--overlay-color': transitionSettings?.overlayColor ?? '#000000',
|
|
} as React.CSSProperties;
|
|
|
|
return {
|
|
isFadingIn,
|
|
resetFadeIn,
|
|
transitionStyle,
|
|
};
|
|
}
|