import React from 'react'
import './GlowingBorder.css'
interface GlowingBorderProps {
children: React.ReactNode
borderWidth?: number // 边框宽度,默认 4px
borderRadius?: number | string // 圆角,默认 8px
className?: string // 额外的类名
style?: React.CSSProperties // 额外的样式
show?: boolean // 是否显示流光动画,默认 false
}
const GlowingBorderComponent: React.FC<GlowingBorderProps> = ({
children,
borderWidth = 2,
borderRadius = 8,
className = '',
style = {},
show = false
}) => {
// 辅助函数:计算圆角值
const getBorderRadiusValue = (value: number | string): string => {
return typeof value === 'number' ? `${value}px` : value
}
// 计算圆角值
const borderRadiusValue = getBorderRadiusValue(borderRadius)
const borderLayerRadius = typeof borderRadius === 'number' ? `${borderRadius + 2}px` : borderRadius
const maskLayerRadius = typeof borderRadius === 'number' ? `${Math.max(0, borderRadius - borderWidth)}px` : borderRadius
const contentBorderRadius = typeof borderRadius === 'number' ? `${Math.max(0, borderRadius - borderWidth)}px` : borderRadius
// 从 style 中提取容器相关的样式(布局相关)
const { width, height, margin, marginTop, marginRight = '2px', marginBottom, marginLeft, ...contentStyle } = style
// 容器样式:包含 padding、尺寸和布局相关样式
const containerStyle: React.CSSProperties = {
padding: `${borderWidth}px`,
...(width && { width }),
...(show && height && { height: '99.9%' }),
...(margin && { margin }),
...(marginTop && { marginTop }),
...(show && marginRight && { marginRight: marginRight }),
...(marginBottom && { marginBottom }),
...(marginLeft && { marginLeft }),
...(show && { boxShadow: '0 0 10px 0 rgba(0, 0, 0, 0.5)' })
}
// CSS 变量对象
const cssVariables: React.CSSProperties = {
'--border-width': `${borderWidth}px`,
'--border-radius': borderRadiusValue,
'--border-layer-radius': borderLayerRadius,
'--mask-layer-radius': maskLayerRadius
} as React.CSSProperties
// 内容区域样式:确保内容不被边框遮挡
const innerStyle: React.CSSProperties = {
position: 'relative',
zIndex: 2,
borderRadius: contentBorderRadius,
...contentStyle
}
return (
<>
{/* SVG 滤镜定义 - 用于粒子效果 */}
<svg style={{ position: 'absolute', width: 0, height: 0 }} aria-hidden='true'>
<filter id='particle-noise' x='-50%' y='-50%' width='200%' height='200%'>
{/* 创建多层噪声,确保整个边框都有均匀的粒子效果 */}
<feTurbulence type='fractalNoise' baseFrequency='2' numOctaves='3' result='noise1' seed='1' />
<feTurbulence type='fractalNoise' baseFrequency='3' numOctaves='2' result='noise2' seed='2' />
{/* 混合两层噪声 */}
{/* <feComposite in='noise1' in2='noise2' operator='multiply' result='combined-noise' /> */}
<feComposite in='noise1' in2='noise2' result='combined-noise' />
{/* 应用位移,让边框边缘有粒子感 */}
<feDisplacementMap in='SourceGraphic' in2='combined-noise' scale='28' xChannelSelector='R' yChannelSelector='G' />
</filter>
</svg>
{/* 容器 */}
<div className={`glowing-border-container ${className}`} style={{ ...cssVariables, ...containerStyle } as React.CSSProperties}>
{/* 发光边框层 - 使用 clip-path 实现流动效果,配合 SVG 粒子效果 */}
<div className='glowing-border-layer glowing-border-layer-1' style={{ opacity: show ? 0.9 : 0 }} />
<div className='glowing-border-layer glowing-border-layer-2' style={{ opacity: show ? 0.9 : 0 }} />
{/* 内部遮罩层 */}
<div className='glowing-border-mask' style={{ opacity: show ? 1 : 0 }} />
{/* 内容区域 */}
<div className='glowing-border-content' style={innerStyle}>
{children}
</div>
</div>
</>
)
}
// 使用 memo 优化性能,避免不必要的重新渲染
const GlowingBorder = React.memo(GlowingBorderComponent)
export default GlowingBorder
GlowingBorder.css
/* SVG 滤镜容器 - 隐藏但保留在 DOM 中 */
.glowing-border-filters {
position: absolute;
width: 0;
height: 0;
overflow: hidden;
}
/* 主容器 */
.glowing-border-container {
position: relative;
display: block; /* 使用 block 而不是 inline-block,避免影响布局计算 */
width: 100%;
height: 100%;
overflow: hidden; /* 裁剪掉边框溢出部分 */
box-sizing: border-box;
/* 性能优化 */
contain: layout style; /* 限制重排和重绘范围 */
transform: translateZ(0); /* 开启硬件加速 */
}
/* 发光边框层 - 使用 conic-gradient mask 实现带虚化的流动线条 */
.glowing-border-layer {
position: absolute;
inset: -2px; /* 略微扩大,让光晕有扩散空间 */
border-radius: var(--border-layer-radius, 10px);
/* 使用圆锥渐变创建渐变色边框 */
/*background: conic-gradient(
from 0deg at 50% 50%,
#ff00c8 0deg,
#ff00c8 60deg,
#006eff 120deg,
#006eff 180deg,
#00ffd9 240deg,
#0e0f0f 300deg,
#ff00c8 360deg
);*/
background: conic-gradient(
from 0deg at 50% 50%,
#006eff 0deg, /* 蓝 */
#006eff 60deg, /* 蓝保持 */
#ff00c8 120deg, /* 粉紫 */
#ff00c8 180deg, /* 粉紫保持 */
#00ffd9 240deg, /* 青 */
#00ffd9 300deg, /* 深色 */
#006eff 360deg /* 蓝闭环 */
);
/* 使用 conic-gradient mask 创建带虚化的线条 */
/* 线条从透明渐变到不透明,再渐变回透明 */
--line-start: 0deg; /* 线条起始位置(动画会改变这个值) */
--fade-size: 30deg; /* 虚化区域大小 */
--line-size: 60deg; /* 实线区域大小 */
mask: conic-gradient(
from var(--line-start) at 50% 50%,
transparent 0deg,
white var(--fade-size),
white calc(var(--fade-size) + var(--line-size)),
transparent calc(var(--fade-size) * 2 + var(--line-size)),
transparent 360deg
);
-webkit-mask: conic-gradient(
from var(--line-start) at 50% 50%,
transparent 0deg,
white var(--fade-size),
white calc(var(--fade-size) + var(--line-size)),
transparent calc(var(--fade-size) * 2 + var(--line-size)),
transparent 360deg
);
z-index: 1; /* 在屏幕内容之下 */
animation: rotateLine 4s infinite linear; /* 旋转线条动画 */
/* 性能优化 */
transform: translateZ(0); /* 开启硬件加速 */
backface-visibility: hidden; /* 防止渲染问题 */
contain: layout style paint; /* 限制重绘范围 */
/* 平滑过渡效果 */
transition: opacity 0.3s ease-in-out;
/* 当透明时禁用交互和动画 */
pointer-events: none;
/* 应用 SVG 粒子效果滤镜,配合模糊形成粒子效果 */
filter: blur(3px) url(#particle-noise);
}
/* 第二个边框层,延迟动画让流动更连续 */
.glowing-border-layer-2 {
animation-delay: -2s; /* 延迟一半时间,让流动更连续 */
}
/* 内部遮罩层 - 保持边框宽度 */
.glowing-border-mask {
position: absolute;
/* inset 决定边框的粗细 */
inset: var(--border-width, 1px);
background-color: transparent; /* 关键:背景透明,透出下面的内容 */
border-radius: var(--mask-layer-radius, 4px);
z-index: 3; /* 确保它在内容和发光边框之上 */
pointer-events: none; /* 允许点击穿透到内容 */
/* 平滑过渡效果 */
transition: opacity 0.3s ease-in-out;
}
/* 内容区域 */
.glowing-border-content {
position: relative;
z-index: 2; /* 确保内容在发光边框之上 */
box-sizing: border-box;
width: 100%;
height: 100%;
}
/* 旋转线条动画 - 通过旋转 mask 实现线条绕边框流动 */
@keyframes rotateLine {
0% {
--line-start: 0deg;
}
100% {
--line-start: 360deg;
}
}
/* 注册 CSS 自定义属性以支持动画 */
@property --line-start {
syntax: '<angle>';
initial-value: 0deg;
inherits: false;
}