"use client"; /* eslint-disable react-hooks/exhaustive-deps */ import { useEffect, useId, useRef, useState, type CSSProperties, type ReactNode } from "react"; import "./GlassSurface.css"; type 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?: "R" | "G" | "B" | "A"; yChannel?: "R" | "G" | "B" | "A"; mixBlendMode?: CSSProperties["mixBlendMode"]; className?: string; style?: CSSProperties; }; export default function GlassSurface({ 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 = {}, }: GlassSurfaceProps) { const uniqueId = useId().replace(/:/g, "-"); const filterId = `glass-filter-${uniqueId}`; const redGradId = `red-grad-${uniqueId}`; const blueGradId = `blue-grad-${uniqueId}`; const [svgSupported, setSvgSupported] = useState(false); 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 = () => { 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); } }); gaussianBlurRef.current?.setAttribute("stdDeviation", displace.toString()); }, [ 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(); }; }, []); useEffect(() => { setTimeout(updateDisplacementMap, 0); }, [width, height]); const supportsSVGFilters = () => { if (typeof window === "undefined" || typeof document === "undefined") { return false; } 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 !== ""; }; useEffect(() => { const frame = requestAnimationFrame(() => { setSvgSupported(supportsSVGFilters()); }); return () => cancelAnimationFrame(frame); }, []); const containerStyle = { ...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}
); }