fixed fades issue in the Safari browser
This commit is contained in:
parent
1e2b72d2bd
commit
4d189346d9
@ -16,9 +16,27 @@
|
||||
:root {
|
||||
--crossfade-duration: 700ms;
|
||||
/* Smooth easing: slow start, gentle acceleration, soft landing */
|
||||
/* Chrome/Firefox: Material Design standard easing */
|
||||
--crossfade-easing: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* Safari-specific easing - Safari's bezier curve rendering is different,
|
||||
use a simpler curve that Safari handles more smoothly */
|
||||
@supports (-webkit-touch-callout: none) {
|
||||
:root {
|
||||
/* Safari: use ease-in-out which Safari renders more consistently */
|
||||
--crossfade-easing: cubic-bezier(0.42, 0, 0.58, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Firefox-specific optimizations */
|
||||
@-moz-document url-prefix() {
|
||||
:root {
|
||||
/* Firefox handles the standard easing well, but we can optimize */
|
||||
--crossfade-easing: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.introjs-tooltip {
|
||||
@apply min-w-[400px] max-w-[480px] p-2 !important;
|
||||
@ -90,32 +108,52 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Crossfade animation classes - GPU accelerated for Safari */
|
||||
/* Crossfade animation classes - GPU accelerated for all browsers */
|
||||
/* Duration controlled by --crossfade-duration CSS variable (single source of truth) */
|
||||
.animate-crossfade-in {
|
||||
/* Explicit initial state prevents Safari flash during animation setup */
|
||||
/* Explicit initial state prevents flash during animation setup */
|
||||
opacity: 0;
|
||||
-webkit-transform: translate3d(0, 0, 0);
|
||||
transform: translate3d(0, 0, 0);
|
||||
-webkit-animation: page-crossfade-in var(--crossfade-duration, 700ms) var(--crossfade-easing, cubic-bezier(0.4, 0, 0.2, 1)) forwards;
|
||||
animation: page-crossfade-in var(--crossfade-duration, 700ms) var(--crossfade-easing, cubic-bezier(0.4, 0, 0.2, 1)) forwards;
|
||||
/* Full animation property for maximum browser compatibility */
|
||||
-webkit-animation-name: page-crossfade-in;
|
||||
animation-name: page-crossfade-in;
|
||||
-webkit-animation-duration: var(--crossfade-duration, 700ms);
|
||||
animation-duration: var(--crossfade-duration, 700ms);
|
||||
-webkit-animation-timing-function: var(--crossfade-easing, cubic-bezier(0.4, 0, 0.2, 1));
|
||||
animation-timing-function: var(--crossfade-easing, cubic-bezier(0.4, 0, 0.2, 1));
|
||||
-webkit-animation-fill-mode: forwards;
|
||||
animation-fill-mode: forwards;
|
||||
-webkit-animation-play-state: running;
|
||||
animation-play-state: running;
|
||||
-webkit-backface-visibility: hidden;
|
||||
backface-visibility: hidden;
|
||||
/* Only animate opacity - transform is for GPU layer promotion */
|
||||
/* Optimize compositing */
|
||||
will-change: opacity;
|
||||
contain: layout style paint;
|
||||
}
|
||||
|
||||
.animate-crossfade-out {
|
||||
/* Explicit initial state prevents Safari flash during animation setup */
|
||||
/* Explicit initial state prevents flash during animation setup */
|
||||
opacity: 1;
|
||||
-webkit-transform: translate3d(0, 0, 0);
|
||||
transform: translate3d(0, 0, 0);
|
||||
-webkit-animation: page-crossfade-out var(--crossfade-duration, 700ms) var(--crossfade-easing, cubic-bezier(0.4, 0, 0.2, 1)) forwards;
|
||||
animation: page-crossfade-out var(--crossfade-duration, 700ms) var(--crossfade-easing, cubic-bezier(0.4, 0, 0.2, 1)) forwards;
|
||||
/* Full animation property for maximum browser compatibility */
|
||||
-webkit-animation-name: page-crossfade-out;
|
||||
animation-name: page-crossfade-out;
|
||||
-webkit-animation-duration: var(--crossfade-duration, 700ms);
|
||||
animation-duration: var(--crossfade-duration, 700ms);
|
||||
-webkit-animation-timing-function: var(--crossfade-easing, cubic-bezier(0.4, 0, 0.2, 1));
|
||||
animation-timing-function: var(--crossfade-easing, cubic-bezier(0.4, 0, 0.2, 1));
|
||||
-webkit-animation-fill-mode: forwards;
|
||||
animation-fill-mode: forwards;
|
||||
-webkit-animation-play-state: running;
|
||||
animation-play-state: running;
|
||||
-webkit-backface-visibility: hidden;
|
||||
backface-visibility: hidden;
|
||||
/* Only animate opacity - transform is for GPU layer promotion */
|
||||
/* Optimize compositing */
|
||||
will-change: opacity;
|
||||
contain: layout style paint;
|
||||
}
|
||||
|
||||
/* Safari-specific GPU compositing optimization */
|
||||
@ -126,8 +164,22 @@
|
||||
-webkit-transform: translate3d(0, 0, 0);
|
||||
transform: translate3d(0, 0, 0);
|
||||
/* Prevent Safari from optimizing away the GPU layer */
|
||||
-webkit-perspective: 1000;
|
||||
perspective: 1000;
|
||||
-webkit-perspective: 1000px;
|
||||
perspective: 1000px;
|
||||
/* Safari performs better with explicit transform-style */
|
||||
-webkit-transform-style: preserve-3d;
|
||||
transform-style: preserve-3d;
|
||||
/* Isolate the layer for better compositing */
|
||||
isolation: isolate;
|
||||
}
|
||||
}
|
||||
|
||||
/* Firefox-specific optimizations */
|
||||
@-moz-document url-prefix() {
|
||||
.animate-crossfade-in,
|
||||
.animate-crossfade-out {
|
||||
/* Firefox handles animations well but benefits from layer isolation */
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -14,6 +14,11 @@
|
||||
* Two modes:
|
||||
* 1. fadeIn mode: Crossfade animation for direct (non-video) page navigation
|
||||
* 2. fadeOut mode: Legacy support - kept for backwards compatibility but not recommended
|
||||
*
|
||||
* Cross-browser notes:
|
||||
* - Chrome: Uses CSS animations with onAnimationEnd event
|
||||
* - Safari: Uses JS timer fallback (Safari's onAnimationEnd can be unreliable)
|
||||
* - Firefox: Uses CSS animations with onAnimationEnd event
|
||||
*/
|
||||
|
||||
import {
|
||||
@ -74,7 +79,7 @@ export interface UseBackgroundTransitionResult {
|
||||
/** Whether page content is currently fading (crossfade in progress) */
|
||||
isFadingIn: boolean;
|
||||
/** Handler to call when fade-in animation ends (pass to onAnimationEnd) */
|
||||
onFadeInAnimationEnd: () => void;
|
||||
onFadeInAnimationEnd: (e?: React.AnimationEvent) => void;
|
||||
/** Reset fade-in state (for cleanup or cancellation) */
|
||||
resetFadeIn: () => void;
|
||||
}
|
||||
@ -121,6 +126,11 @@ export function useBackgroundTransition({
|
||||
// Track previous isSwitching state to detect transition start
|
||||
const wasSwitchingRef = useRef(false);
|
||||
|
||||
// Safari fallback timer ref - Safari's onAnimationEnd can be unreliable
|
||||
const fadeInTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
// Track if animation was already completed (by event or timer)
|
||||
const fadeInCompletedRef = useRef(false);
|
||||
|
||||
/**
|
||||
* Reset fade-out state before starting a new transition.
|
||||
* This prevents the fade-out effect from re-triggering when state resets.
|
||||
@ -134,15 +144,44 @@ export function useBackgroundTransition({
|
||||
*/
|
||||
const resetFadeIn = useCallback(() => {
|
||||
setIsFadingIn(false);
|
||||
fadeInCompletedRef.current = false;
|
||||
if (fadeInTimerRef.current) {
|
||||
clearTimeout(fadeInTimerRef.current);
|
||||
fadeInTimerRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Complete fade-in animation.
|
||||
* Called either by onAnimationEnd or by timer fallback.
|
||||
* Uses ref to prevent double-completion.
|
||||
*/
|
||||
const completeFadeIn = useCallback(() => {
|
||||
if (fadeInCompletedRef.current) return;
|
||||
fadeInCompletedRef.current = true;
|
||||
|
||||
// Clear backup timer if it exists
|
||||
if (fadeInTimerRef.current) {
|
||||
clearTimeout(fadeInTimerRef.current);
|
||||
fadeInTimerRef.current = null;
|
||||
}
|
||||
|
||||
setIsFadingIn(false);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Handler for onAnimationEnd event.
|
||||
* Called when CSS animation completes - CSS is the source of truth for duration.
|
||||
* Called when CSS animation completes.
|
||||
* Uses completeFadeIn to handle deduplication with timer fallback.
|
||||
*/
|
||||
const onFadeInAnimationEnd = useCallback(() => {
|
||||
setIsFadingIn(false);
|
||||
}, []);
|
||||
const onFadeInAnimationEnd = useCallback(
|
||||
(e?: React.AnimationEvent) => {
|
||||
// Only handle the crossfade animation, not child animations
|
||||
if (e && e.animationName !== 'page-crossfade-in') return;
|
||||
completeFadeIn();
|
||||
},
|
||||
[completeFadeIn],
|
||||
);
|
||||
|
||||
/**
|
||||
* Effect: Fade out and remove transition overlay when background is ready.
|
||||
@ -240,6 +279,10 @@ export function useBackgroundTransition({
|
||||
* preventing any flash of new content at full opacity.
|
||||
*
|
||||
* IMPORTANT: Skip this for transitions - transition video IS the effect.
|
||||
*
|
||||
* Cross-browser handling:
|
||||
* - Sets up JS timer fallback for Safari (unreliable onAnimationEnd)
|
||||
* - Chrome/Firefox rely on CSS onAnimationEnd event
|
||||
*/
|
||||
useLayoutEffect(() => {
|
||||
if (!fadeIn) {
|
||||
@ -256,9 +299,41 @@ export function useBackgroundTransition({
|
||||
// Only start crossfade for NON-transition navigation
|
||||
// Transitions use video overlay - no fade needed
|
||||
if (justStartedSwitching && !hasActiveTransition) {
|
||||
// Reset completion flag for new animation
|
||||
fadeInCompletedRef.current = false;
|
||||
|
||||
// Clear any existing timer
|
||||
if (fadeInTimerRef.current) {
|
||||
clearTimeout(fadeInTimerRef.current);
|
||||
fadeInTimerRef.current = null;
|
||||
}
|
||||
|
||||
setIsFadingIn(true);
|
||||
|
||||
// Safari/Firefox fallback: Use JS timer as backup since onAnimationEnd
|
||||
// can be unreliable or fire on wrong animations.
|
||||
// Timer is slightly longer than CSS duration to let CSS complete first.
|
||||
// Chrome typically fires onAnimationEnd reliably, but timer is harmless backup.
|
||||
const duration = getCrossfadeDuration();
|
||||
// Add 50ms buffer for Safari's animation timing variance
|
||||
const bufferMs = isSafari() ? 100 : 50;
|
||||
|
||||
fadeInTimerRef.current = setTimeout(() => {
|
||||
fadeInTimerRef.current = null;
|
||||
completeFadeIn();
|
||||
}, duration + bufferMs);
|
||||
}
|
||||
}, [pageSwitch.isSwitching, fadeIn]);
|
||||
}, [pageSwitch.isSwitching, fadeIn, completeFadeIn]);
|
||||
|
||||
// Cleanup timer on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (fadeInTimerRef.current) {
|
||||
clearTimeout(fadeInTimerRef.current);
|
||||
fadeInTimerRef.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
isOverlayFadingOut,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user