Files
flxn-app/src/components/glitch-avatar.tsx
T
2026-07-14 00:39:38 -07:00

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;