Files
pfm/PostFundManagement.PartnerUI/src/components/common/BorderGlow.tsx
T
2026-09-28 16:13:42 +02:00

193 lines
6.6 KiB
TypeScript

import React, { useRef, useCallback, useEffect, ReactNode } from 'react';
import './BorderGlow.css';
export interface BorderGlowProps {
children?: ReactNode;
className?: string;
edgeSensitivity?: number;
glowColor?: string;
backgroundColor?: string;
borderRadius?: number;
glowRadius?: number;
glowIntensity?: number;
coneSpread?: number;
animated?: boolean;
colors?: string[];
fillOpacity?: number;
}
function parseHSL(hslStr: string) {
const match = hslStr.match(/([\d.]+)\s*([\d.]+)%?\s*([\d.]+)%?/);
if (!match) return { h: 40, s: 80, l: 80 };
return { h: parseFloat(match[1]), s: parseFloat(match[2]), l: parseFloat(match[3]) };
}
function buildGlowVars(glowColor: string, intensity: number) {
const { h, s, l } = parseHSL(glowColor);
const base = `${h}deg ${s}% ${l}%`;
const opacities = [100, 60, 50, 40, 30, 20, 10];
const keys = ['', '-60', '-50', '-40', '-30', '-20', '-10'];
const vars: Record<string, string> = {};
for (let i = 0; i < opacities.length; i++) {
vars[`--glow-color${keys[i]}`] = `hsl(${base} / ${Math.min(opacities[i] * intensity, 100)}%)`;
}
return vars;
}
const GRADIENT_POSITIONS = ['80% 55%', '69% 34%', '8% 6%', '41% 38%', '86% 85%', '82% 18%', '51% 4%'];
const GRADIENT_KEYS = ['--gradient-one', '--gradient-two', '--gradient-three', '--gradient-four', '--gradient-five', '--gradient-six', '--gradient-seven'];
const COLOR_MAP = [0, 1, 2, 0, 1, 2, 1];
function buildGradientVars(colors: string[]) {
const vars: Record<string, string> = {};
for (let i = 0; i < 7; i++) {
const c = colors[Math.min(COLOR_MAP[i], colors.length - 1)];
vars[GRADIENT_KEYS[i]] = `radial-gradient(at ${GRADIENT_POSITIONS[i]}, ${c} 0px, transparent 50%)`;
}
vars['--gradient-base'] = `linear-gradient(${colors[0]} 0 100%)`;
return vars;
}
function isLightColor(color: string) {
const value = color.trim().replace('#', '');
if (!/^[\da-f]{3}([\da-f]{3})?$/i.test(value)) return false;
const hex = value.length === 3 ? value.split('').map(char => char + char).join('') : value;
const red = parseInt(hex.slice(0, 2), 16);
const green = parseInt(hex.slice(2, 4), 16);
const blue = parseInt(hex.slice(4, 6), 16);
return red * 0.2126 + green * 0.7152 + blue * 0.0722 > 180;
}
function easeOutCubic(x: number) { return 1 - Math.pow(1 - x, 3); }
function easeInCubic(x: number) { return x * x * x; }
interface AnimateOptions {
start?: number;
end?: number;
duration?: number;
delay?: number;
ease?: (x: number) => number;
onUpdate: (val: number) => void;
onEnd?: () => void;
}
function animateValue({ start = 0, end = 100, duration = 1000, delay = 0, ease = easeOutCubic, onUpdate, onEnd }: AnimateOptions) {
const t0 = performance.now() + delay;
function tick() {
const elapsed = performance.now() - t0;
const t = Math.min(elapsed / duration, 1);
onUpdate(start + (end - start) * ease(t));
if (t < 1) requestAnimationFrame(tick);
else if (onEnd) onEnd();
}
setTimeout(() => requestAnimationFrame(tick), delay);
}
export const BorderGlow: React.FC<BorderGlowProps> = ({
children,
className = '',
edgeSensitivity = 30,
glowColor = '160 80 70',
backgroundColor = '#0f172a',
borderRadius = 20,
glowRadius = 35,
glowIntensity = 1.0,
coneSpread = 25,
animated = false,
colors = ['#34d399', '#2dd4bf', '#38bdf8'],
fillOpacity = 0.4,
}) => {
const cardRef = useRef<HTMLDivElement>(null);
const getCenterOfElement = useCallback((el: HTMLElement) => {
const { width, height } = el.getBoundingClientRect();
return [width / 2, height / 2];
}, []);
const getEdgeProximity = useCallback((el: HTMLElement, x: number, y: number) => {
const [cx, cy] = getCenterOfElement(el);
const dx = x - cx;
const dy = y - cy;
let kx = Infinity;
let ky = Infinity;
if (dx !== 0) kx = cx / Math.abs(dx);
if (dy !== 0) ky = cy / Math.abs(dy);
return Math.min(Math.max(1 / Math.min(kx, ky), 0), 1);
}, [getCenterOfElement]);
const getCursorAngle = useCallback((el: HTMLElement, x: number, y: number) => {
const [cx, cy] = getCenterOfElement(el);
const dx = x - cx;
const dy = y - cy;
if (dx === 0 && dy === 0) return 0;
const radians = Math.atan2(dy, dx);
let degrees = radians * (180 / Math.PI) + 90;
if (degrees < 0) degrees += 360;
return degrees;
}, [getCenterOfElement]);
const handlePointerMove = useCallback((e: React.PointerEvent<HTMLDivElement>) => {
const card = cardRef.current;
if (!card) return;
const rect = card.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const edge = getEdgeProximity(card, x, y);
const angle = getCursorAngle(card, x, y);
card.style.setProperty('--edge-proximity', `${(edge * 100).toFixed(3)}`);
card.style.setProperty('--cursor-angle', `${angle.toFixed(3)}deg`);
}, [getEdgeProximity, getCursorAngle]);
useEffect(() => {
if (!animated || !cardRef.current) return;
const card = cardRef.current;
const angleStart = 110;
const angleEnd = 465;
card.classList.add('sweep-active');
card.style.setProperty('--cursor-angle', `${angleStart}deg`);
animateValue({ duration: 500, onUpdate: v => card.style.setProperty('--edge-proximity', String(v)) });
animateValue({ ease: easeInCubic, duration: 1500, end: 50, onUpdate: v => {
card.style.setProperty('--cursor-angle', `${(angleEnd - angleStart) * (v / 100) + angleStart}deg`);
}});
animateValue({ ease: easeOutCubic, delay: 1500, duration: 2250, start: 50, end: 100, onUpdate: v => {
card.style.setProperty('--cursor-angle', `${(angleEnd - angleStart) * (v / 100) + angleStart}deg`);
}});
animateValue({ ease: easeInCubic, delay: 2500, duration: 1500, start: 100, end: 0,
onUpdate: v => card.style.setProperty('--edge-proximity', String(v)),
onEnd: () => card.classList.remove('sweep-active'),
});
}, [animated]);
const glowVars = buildGlowVars(glowColor, glowIntensity);
const lightSurface = isLightColor(backgroundColor);
return (
<div
ref={cardRef}
onPointerMove={handlePointerMove}
className={`border-glow-card${lightSurface ? ' border-glow-card--light' : ''} ${className}`}
style={{
'--card-bg': backgroundColor,
'--edge-sensitivity': edgeSensitivity,
'--border-radius': `${borderRadius}px`,
'--glow-padding': `${glowRadius}px`,
'--cone-spread': coneSpread,
'--fill-opacity': fillOpacity,
...glowVars,
...buildGradientVars(colors),
} as React.CSSProperties}
>
<span className="edge-light" />
<div className="border-glow-inner">
{children}
</div>
</div>
);
};
export default BorderGlow;