agentbond-api / frontend /src /components /FadingVideo.jsx
Karan6124's picture
implement rAF-driven crossfading FadingVideo component
139f946
Raw
History Blame Contribute Delete
3.47 kB
import React, { useEffect, useRef } from "react";
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 (fade starts 0.55s before video ends)
const fadeTo = (targetOpacity, durationMs) => {
const video = videoRef.current;
if (!video) return;
// Cancel any active animation frame
if (rafIdRef.current) {
cancelAnimationFrame(rafIdRef.current);
}
// Read current opacity from the inline style of the video element
const currentOpacityStr = video.style.opacity;
const startOpacity = currentOpacityStr ? parseFloat(currentOpacityStr) : 0;
const opacityDiff = targetOpacity - startOpacity;
const startTime = performance.now();
const animate = (now) => {
const elapsed = now - startTime;
const progress = Math.min(elapsed / durationMs, 1);
// Set interpolated opacity
video.style.opacity = (startOpacity + opacityDiff * progress).toString();
if (progress < 1) {
rafIdRef.current = requestAnimationFrame(animate);
} else {
rafIdRef.current = null;
}
};
rafIdRef.current = requestAnimationFrame(animate);
};
useEffect(() => {
const video = videoRef.current;
if (!video) return;
// Initialize state
video.style.opacity = "0";
fadingOutRef.current = false;
const handleLoadedData = () => {
video.style.opacity = "0";
video.play().catch((err) => console.log("Video play interrupted:", err));
fadeTo(1, FADE_MS);
};
const handleTimeUpdate = () => {
const duration = video.duration;
const currentTime = video.currentTime;
if (duration && !fadingOutRef.current) {
const timeLeft = duration - currentTime;
// If we are within the lead window, initiate fade out
if (timeLeft <= FADE_OUT_LEAD && timeLeft > 0) {
fadingOutRef.current = true;
fadeTo(0, FADE_MS);
}
}
};
const handleEnded = () => {
video.style.opacity = "0";
// Wait 100ms, reset to start, then play and fade back in
setTimeout(() => {
if (!video) return;
video.currentTime = 0;
video.play()
.then(() => {
fadingOutRef.current = false;
fadeTo(1, FADE_MS);
})
.catch((err) => console.log("Video loop playback failed:", err));
}, 100);
};
// Attach custom event listeners
video.addEventListener("loadeddata", handleLoadedData);
video.addEventListener("timeupdate", handleTimeUpdate);
video.addEventListener("ended", handleEnded);
// Trigger loadeddata logic manually if browser already has it cached
if (video.readyState >= 2) {
handleLoadedData();
}
// Clean up animation frames and listeners on unmount
return () => {
video.removeEventListener("loadeddata", handleLoadedData);
video.removeEventListener("timeupdate", handleTimeUpdate);
video.removeEventListener("ended", handleEnded);
if (rafIdRef.current) {
cancelAnimationFrame(rafIdRef.current);
}
};
}, [src]);
return (
<video
ref={videoRef}
src={src}
className={className}
style={{ ...style, transition: "none" }}
muted
playsInline
autoPlay
preload="auto"
/>
);
}