158 lines
3.9 KiB
TypeScript
158 lines
3.9 KiB
TypeScript
import { useState, useEffect, useRef } from "react";
|
|
import { Box, Avatar as MantineAvatar } from "@mantine/core";
|
|
|
|
interface GlitchAvatarProps {
|
|
name: string;
|
|
src?: string;
|
|
glitchSrc?: string;
|
|
size?: number;
|
|
radius?: string | number;
|
|
withBorder?: boolean;
|
|
children?: React.ReactNode;
|
|
}
|
|
|
|
const FRAME_PADDING = 8;
|
|
|
|
const toCssRadius = (radius: string | number) =>
|
|
typeof radius === "number" ? `${radius}px` : `var(--mantine-radius-${radius})`;
|
|
|
|
const GlitchAvatar = ({
|
|
name,
|
|
src,
|
|
glitchSrc,
|
|
size = 35,
|
|
radius = "md",
|
|
withBorder = true,
|
|
children,
|
|
}: GlitchAvatarProps) => {
|
|
const [showGlitch, setShowGlitch] = useState(false);
|
|
const [isPlaying, setIsPlaying] = useState(false);
|
|
const videoRef = useRef<HTMLVideoElement>(null);
|
|
|
|
useEffect(() => {
|
|
if (!glitchSrc) return;
|
|
|
|
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
|
|
|
let timeoutId: ReturnType<typeof setTimeout>;
|
|
const scheduleNextGlitch = () => {
|
|
const delay = Math.random() * 10000 + 5000;
|
|
timeoutId = setTimeout(() => {
|
|
setShowGlitch(true);
|
|
setIsPlaying(true);
|
|
|
|
timeoutId = setTimeout(() => {
|
|
setShowGlitch(false);
|
|
setIsPlaying(false);
|
|
scheduleNextGlitch();
|
|
}, 4000);
|
|
}, delay);
|
|
};
|
|
|
|
scheduleNextGlitch();
|
|
return () => clearTimeout(timeoutId);
|
|
}, [glitchSrc]);
|
|
|
|
useEffect(() => {
|
|
const video = videoRef.current;
|
|
if (!video) return;
|
|
|
|
const handleEnded = () => {
|
|
setShowGlitch(false);
|
|
setIsPlaying(false);
|
|
};
|
|
|
|
video.addEventListener("ended", handleEnded);
|
|
return () => video.removeEventListener("ended", handleEnded);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const video = videoRef.current;
|
|
if (!video) return;
|
|
|
|
video.load();
|
|
}, [glitchSrc]);
|
|
|
|
useEffect(() => {
|
|
const video = videoRef.current;
|
|
if (!video || !showGlitch || !isPlaying) return;
|
|
|
|
video.currentTime = 0;
|
|
video.play().catch((err) => {
|
|
console.error("Failed to play glitch", err);
|
|
});
|
|
}, [showGlitch, isPlaying]);
|
|
|
|
const innerRadius = toCssRadius(radius);
|
|
|
|
return (
|
|
<Box
|
|
style={{
|
|
position: "relative",
|
|
width: "fit-content",
|
|
padding: FRAME_PADDING,
|
|
border: withBorder
|
|
? "1px solid var(--mantine-color-default-border)"
|
|
: "1px solid transparent",
|
|
borderRadius: `calc(${innerRadius} + ${FRAME_PADDING}px)`,
|
|
}}
|
|
>
|
|
{src ? (
|
|
<Box style={{ position: "relative" }}>
|
|
<img
|
|
src={src}
|
|
alt={name}
|
|
style={{
|
|
display: "block",
|
|
maxWidth: size,
|
|
maxHeight: size,
|
|
width: "auto",
|
|
height: "auto",
|
|
borderRadius: innerRadius,
|
|
opacity: showGlitch ? 0 : 1,
|
|
transition: showGlitch
|
|
? "opacity 0.05s ease-in"
|
|
: "opacity 0.25s ease-out",
|
|
}}
|
|
/>
|
|
{glitchSrc && (
|
|
<video
|
|
ref={videoRef}
|
|
src={glitchSrc}
|
|
style={{
|
|
position: "absolute",
|
|
inset: 0,
|
|
width: "100%",
|
|
height: "100%",
|
|
objectFit: "contain",
|
|
borderRadius: innerRadius,
|
|
opacity: showGlitch ? 1 : 0,
|
|
visibility: showGlitch ? "visible" : "hidden",
|
|
transition: showGlitch ? "opacity 0.05s ease-in" : "none",
|
|
pointerEvents: "none",
|
|
}}
|
|
muted
|
|
playsInline
|
|
preload="auto"
|
|
/>
|
|
)}
|
|
</Box>
|
|
) : (
|
|
<MantineAvatar
|
|
alt={name}
|
|
key={name}
|
|
name={name}
|
|
color="initials"
|
|
size={size}
|
|
radius={radius}
|
|
w={size}
|
|
>
|
|
{children}
|
|
</MantineAvatar>
|
|
)}
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
export default GlitchAvatar;
|