import { useEffect, useRef, useId, ReactNode, CSSProperties } from 'react'; import './GlassSurface.css'; interface GlassSurfaceProps { children: ReactNode; width?: number | string; height?: number | string; borderRadius?: number; borderWidth?: number; brightness?: number; opacity?: number; blur?: number; displace?: number; backgroundOpacity?: number; saturation?: number; distortionScale?: number; redOffset?: number; greenOffset?: number; blueOffset?: number; xChannel?: string; yChannel?: string; mixBlendMode?: string; className?: string; style?: CSSProperties; } const GlassSurface: React.FC = ({ children, width = 200, height = 80, borderRadius = 20, borderWidth = 0.07, brightness = 50, opacity = 0.93, blur = 11, displace = 0, backgroundOpacity = 0, saturation = 1, distortionScale = -180, redOffset = 0, greenOffset = 10, blueOffset = 20, xChannel = 'R', yChannel = 'G', mixBlendMode = 'difference', className = '', style = {} }) => { const uniqueId = useId().replace(/:/g, '-'); const filterId = `glass-filter-${uniqueId}`; const redGradId = `red-grad-${uniqueId}`; const blueGradId = `blue-grad-${uniqueId}`; const containerRef = useRef(null); const feImageRef = useRef(null); const redChannelRef = useRef(null); const greenChannelRef = useRef(null); const blueChannelRef = useRef(null); const gaussianBlurRef = useRef(null); const generateDisplacementMap = () => { const rect = containerRef.current?.getBoundingClientRect(); const actualWidth = rect?.width || 400; const actualHeight = rect?.height || 200; const edgeSize = Math.min(actualWidth, actualHeight) * (borderWidth * 0.5); const svgContent = ` `; return `data:image/svg+xml,${encodeURIComponent(svgContent)}`; }; const updateDisplacementMap = () => { if (feImageRef.current) { feImageRef.current.setAttribute('href', generateDisplacementMap()); } }; useEffect(() => { updateDisplacementMap(); [ { ref: redChannelRef, offset: redOffset }, { ref: greenChannelRef, offset: greenOffset }, { ref: blueChannelRef, offset: blueOffset } ].forEach(({ ref, offset }) => { if (ref.current) { ref.current.setAttribute('scale', (distortionScale + offset).toString()); ref.current.setAttribute('xChannelSelector', xChannel); ref.current.setAttribute('yChannelSelector', yChannel); } }); if (gaussianBlurRef.current) { gaussianBlurRef.current.setAttribute('stdDeviation', displace.toString()); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [ width, height, borderRadius, borderWidth, brightness, opacity, blur, displace, distortionScale, redOffset, greenOffset, blueOffset, xChannel, yChannel, mixBlendMode ]); useEffect(() => { if (!containerRef.current) return; const resizeObserver = new ResizeObserver(() => { setTimeout(updateDisplacementMap, 0); }); resizeObserver.observe(containerRef.current); return () => { resizeObserver.disconnect(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { setTimeout(updateDisplacementMap, 0); // eslint-disable-next-line react-hooks/exhaustive-deps }, [width, height]); const supportsSVGFilters = () => { const isWebkit = /Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent); const isFirefox = /Firefox/.test(navigator.userAgent); if (isWebkit || isFirefox) { return false; } const div = document.createElement('div'); div.style.backdropFilter = `url(#${filterId})`; return div.style.backdropFilter !== ''; }; const containerStyle: CSSProperties = { ...style, width: typeof width === 'number' ? `${width}px` : width, height: typeof height === 'number' ? `${height}px` : height, borderRadius: `${borderRadius}px`, '--glass-frost': backgroundOpacity, '--glass-saturation': saturation, '--filter-id': `url(#${filterId})` } as CSSProperties; return (
{children}
); }; export default GlassSurface;