GlowingBorder

13 阅读4分钟
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;
}