multisense_df / app /frontend /src /components /FadingVideo.jsx
vijay-2132's picture
Clean deployment without large checkpoints
a94bd47
Raw
History Blame Contribute Delete
3.34 kB
import { useEffect, useRef } from 'react';
/**
* FadingVideo Component
* Custom requestAnimationFrame crossfade logic for looping background videos.
* Opacity fades out at 0.55s before video ends and fades back in on loop start.
*/
export default function FadingVideo({ src, className, style }) {
const videoRef = useRef(null);
const rafIdRef = useRef(null);
const fadingOutRef = useRef(false);
const FADE_MS = 500;
const FADE_OUT_LEAD = 0.55; // seconds before end to start fading out
const fadeTo = (targetOpacity, duration) => {
if (!videoRef.current) return;
const video = videoRef.current;
// Cancel any ongoing animation frame
if (rafIdRef.current) {
cancelAnimationFrame(rafIdRef.current);
}
const currentOpacity = parseFloat(video.style.opacity) || 0;
const opacityDifference = targetOpacity - currentOpacity;
if (opacityDifference === 0) return;
const startTime = performance.now();
const animate = (currentTime) => {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
// Calculate next opacity
const nextOpacity = currentOpacity + opacityDifference * progress;
if (videoRef.current) {
videoRef.current.style.opacity = nextOpacity.toString();
}
if (progress < 1) {
rafIdRef.current = requestAnimationFrame(animate);
}
};
rafIdRef.current = requestAnimationFrame(animate);
};
useEffect(() => {
const video = videoRef.current;
if (!video) return;
// Initially transparent
video.style.opacity = '0';
const handleLoadedData = () => {
video.style.opacity = '0';
video.play().catch(() => {});
fadeTo(1, FADE_MS);
};
const handleTimeUpdate = () => {
if (!video.duration) return;
const remainingTime = video.duration - video.currentTime;
// Start fade out at 0.55 seconds before end
if (!fadingOutRef.current && remainingTime <= FADE_OUT_LEAD && remainingTime > 0) {
fadingOutRef.current = true;
fadeTo(0, FADE_MS);
}
};
const handleEnded = () => {
video.style.opacity = '0';
setTimeout(() => {
if (!videoRef.current) return;
videoRef.current.currentTime = 0;
videoRef.current.play().catch(() => {});
fadingOutRef.current = false;
fadeTo(1, FADE_MS);
}, 100);
};
// Listeners
video.addEventListener('loadeddata', handleLoadedData);
video.addEventListener('timeupdate', handleTimeUpdate);
video.addEventListener('ended', handleEnded);
// If already loaded/ready
if (video.readyState >= 2) {
handleLoadedData();
}
return () => {
if (rafIdRef.current) {
cancelAnimationFrame(rafIdRef.current);
}
video.removeEventListener('loadeddata', handleLoadedData);
video.removeEventListener('timeupdate', handleTimeUpdate);
video.removeEventListener('ended', handleEnded);
};
}, [src]);
return (
<video
ref={videoRef}
src={src}
className={className}
style={{
...style,
transition: 'none', // Ensure NO CSS transitions interfere with our rAF fades
}}
autoPlay
muted
playsInline
preload="auto"
/>
);
}