import { useEffect, useRef, memo } from 'react' import * as PIXI from 'pixi.js' import { BlurFilter } from 'pixi.js'
/**
- 蓝紫科技感流光边框组件
- 特性:
-
- 模糊渐变边框
-
- 蓝色到紫色的科技感流动
-
- 强烈的内外发光效果
-
- 平滑的光斑过渡 */
interface GlowBorderWrapperProps { children?: React.ReactNode enabled?: boolean borderWidth?: number borderRadius?: number glowIntensity?: number flowSpeed?: number baseColor?: string midColor?: string highlightColor?: string zIndex?: number show?: boolean }
const hexToColor = (hex: string): number => { return parseInt(hex.replace('#', '0x')) }
const lerpColor = (c1: number, c2: number, t: number): number => { const r1 = (c1 >> 16) & 0xff const g1 = (c1 >> 8) & 0xff const b1 = c1 & 0xff const r2 = (c2 >> 16) & 0xff const g2 = (c2 >> 8) & 0xff const b2 = c2 & 0xff
const r = Math.floor(r1 + (r2 - r1) * t) & 0xff
const g = Math.floor(g1 + (g2 - g1) * t) & 0xff
const b = Math.floor(b1 + (b2 - b1) * t) & 0xff
return (r << 16) + (g << 8) + b
}
const GlowBorderWrapper: React.FC = memo( ({ children, enabled = true, borderWidth = 1.5, borderRadius = 18, glowIntensity = 15, flowSpeed = 0.2, baseColor = '#5295FA', // 蓝色 midColor = '#9F65F1', // 紫色 highlightColor = '#C084FC', // 亮紫色 zIndex = 0, show = false }) => { const containerRef = useRef(null) const canvasContainerRef = useRef(null) const appRef = useRef<PIXI.Application | null>(null) const rafRef = useRef<number | null>(null)
// PIXI 应用初始化 - 只在组件挂载时执行一次
useEffect(() => {
if (!enabled || !canvasContainerRef.current) return
const el = canvasContainerRef.current
let cleanup: (() => void) | null = null
const initApp = async () => {
// 使用 containerRef 的尺寸而不是 el 的尺寸
const initialWidth = containerRef.current?.offsetWidth || 1
const initialHeight = containerRef.current?.offsetHeight || 1
const app = new PIXI.Application()
await app.init({
width: initialWidth,
height: initialHeight,
backgroundAlpha: 0,
antialias: true,
resolution: window.devicePixelRatio || 1,
autoDensity: true
})
appRef.current = app
const canvas = app.canvas as HTMLCanvasElement
canvas.style.position = 'absolute'
canvas.style.inset = '0'
canvas.style.pointerEvents = 'none'
// 不设置 canvas 自己的 zIndex,让它继承容器的层级
el.appendChild(canvas)
// 创建容器
const borderContainer = new PIXI.Container()
app.stage.addChild(borderContainer)
// 静态基础边框(形成闭环)
const staticBorder = new PIXI.Graphics()
borderContainer.addChild(staticBorder)
// 蓝色光斑边框
const blueFlowBorder = new PIXI.Graphics()
borderContainer.addChild(blueFlowBorder)
// 紫色光斑边框
const purpleFlowBorder = new PIXI.Graphics()
borderContainer.addChild(purpleFlowBorder)
// 内层发光
const innerGlow = new PIXI.Graphics()
borderContainer.addChild(innerGlow)
// 外层超大范围柔和发光层
const outerGlow = new PIXI.Graphics()
borderContainer.addChild(outerGlow)
// 中层发光层
const midGlow = new PIXI.Graphics()
borderContainer.addChild(midGlow)
// 添加多层模糊滤镜以增强发光效果 - 柔和但不会太大
const blueGlow = new BlurFilter({
strength: glowIntensity * 1.2,
quality: 6
})
const purpleGlow = new BlurFilter({
strength: glowIntensity * 1.2,
quality: 6
})
const softGlow = new BlurFilter({
strength: glowIntensity * 0.6,
quality: 5
})
// 外层柔和模糊 - 适中范围
const outerBlur = new BlurFilter({
strength: glowIntensity * 1.8,
quality: 8
})
// 中层模糊
const midBlur = new BlurFilter({
strength: glowIntensity * 1.4,
quality: 6
})
blueFlowBorder.filters = [blueGlow]
purpleFlowBorder.filters = [purpleGlow]
innerGlow.filters = [softGlow]
outerGlow.filters = [outerBlur]
midGlow.filters = [midBlur]
// 缓存尺寸参数,避免每帧重复计算
let cachedWidth = 0
let cachedHeight = 0
let cachedPerimeter = 0
let cachedW = 0
let cachedH = 0
const padding = 4
// 更新缓存的尺寸参数
const updateCachedDimensions = () => {
const currentWidth = appRef.current?.screen.width || 1
const currentHeight = appRef.current?.screen.height || 1
if (currentWidth !== cachedWidth || currentHeight !== cachedHeight) {
cachedWidth = currentWidth
cachedHeight = currentHeight
cachedW = currentWidth - padding * 2
cachedH = currentHeight - padding * 2
cachedPerimeter = (cachedW + cachedH) * 2
}
}
// 根据距离获取边框上的点 - 使用缓存的尺寸
const getBorderPoint = (distance: number) => {
let d = distance % cachedPerimeter
if (d < 0) d += cachedPerimeter
let x: number, y: number
if (d < cachedW) {
x = padding + d
y = padding
} else if (d < cachedW + cachedH) {
x = padding + cachedW
y = padding + (d - cachedW)
} else if (d < cachedW * 2 + cachedH) {
x = padding + cachedW - (d - cachedW - cachedH)
y = padding + cachedH
} else {
x = padding
y = padding + cachedH - (d - cachedW * 2 - cachedH)
}
return { x, y }
}
// 初始化缓存
updateCachedDimensions()
// 绘制流光边框
const drawFlowBorder = (phase: number) => {
// 只在尺寸变化时更新缓存
updateCachedDimensions()
blueFlowBorder.clear()
purpleFlowBorder.clear()
innerGlow.clear()
outerGlow.clear()
midGlow.clear()
const steps = 400
// 蓝色系渐变
const blueBase = hexToColor('#5295FA ') // 深蓝
const blueMid = hexToColor('#3B82F6') // 亮蓝
const blueHi = hexToColor('#5295FA') // 青蓝
// 紫色系渐变
const purpleBase = hexToColor('#9455EE') // 深紫
const purpleMid = hexToColor('#A855F7') // 亮紫
const purpleHi = hexToColor('#9455EE') // 淡紫
// 归一化相位
const normalizedPhase = phase % (Math.PI * 2)
for (let i = 0; i < steps; i++) {
const t1 = i / steps
const t2 = (i + 1) / steps
const d1 = cachedPerimeter * t1
const d2 = cachedPerimeter * t2
const p1 = getBorderPoint(d1)
const p2 = getBorderPoint(d2)
// 计算当前位置的角度
const currentAngle = t1 * Math.PI * 2
// 计算位置相对于相位的偏移
let angleOffset = (currentAngle - normalizedPhase + Math.PI * 2) % (Math.PI * 2)
// 整个圆周形成渐变:蓝色 -> 紫色 -> 蓝色(完整闭环)
let color: number
let intensity: number
// 归一化角度到 0-1
const normalizedAngle = angleOffset / (Math.PI * 2)
// 创建两个光斑区域,扩大覆盖范围,几乎没有间隙
let distToSpot1 = Math.abs(normalizedAngle - 0)
let distToSpot2 = Math.abs(normalizedAngle - 0.5)
if (distToSpot2 > 0.5) distToSpot2 = 1 - distToSpot2
if (distToSpot1 > 0.5) distToSpot1 = 1 - distToSpot1
const minDist = Math.min(distToSpot1, distToSpot2)
// 光斑覆盖范围适中
intensity = Math.max(0, 1 - minDist / 0.48)
// 使用平缓的衰减曲线,让光影过渡柔和
intensity = Math.pow(intensity, 1.0)
// 颜色渐变:根据角度在蓝紫之间循环
if (normalizedAngle < 0.25) {
// 0-0.25: 蓝色到紫色过渡
const t = normalizedAngle / 0.25
if (t < 0.5) {
color = lerpColor(blueMid, blueHi, t * 2)
} else {
color = lerpColor(blueHi, purpleBase, (t - 0.5) * 2)
}
} else if (normalizedAngle < 0.5) {
// 0.25-0.5: 紫色
const t = (normalizedAngle - 0.25) / 0.25
color = lerpColor(purpleBase, purpleMid, t)
} else if (normalizedAngle < 0.75) {
// 0.5-0.75: 紫色到蓝色过渡
const t = (normalizedAngle - 0.5) / 0.25
if (t < 0.5) {
color = lerpColor(purpleMid, purpleHi, t * 2)
} else {
color = lerpColor(purpleHi, blueBase, (t - 0.5) * 2)
}
} else {
// 0.75-1: 蓝色
const t = (normalizedAngle - 0.75) / 0.25
color = lerpColor(blueBase, blueMid, t)
}
if (intensity > 0.02) {
// 线宽随强度变化
const widthDynamic = borderWidth * (1.2 + intensity * 1.2)
// 透明度让光影柔和,过渡平滑
const alphaDynamic = 0.3 + 0.45 * Math.pow(intensity, 1.0)
// 根据颜色选择图层(简单根据角度判断)
const targetBorder = normalizedAngle < 0.5 ? blueFlowBorder : purpleFlowBorder
targetBorder.moveTo(p1.x, p1.y).lineTo(p2.x, p2.y).stroke({
width: widthDynamic,
color,
alpha: alphaDynamic
})
// 外层柔和发光 - 适中宽度
const outerWidth = widthDynamic * 2
const outerAlpha = alphaDynamic * 0.2
outerGlow.moveTo(p1.x, p1.y).lineTo(p2.x, p2.y).stroke({
width: outerWidth,
color,
alpha: outerAlpha
})
// 中层发光
const midWidth = widthDynamic * 1.5
const midAlpha = alphaDynamic * 0.3
midGlow.moveTo(p1.x, p1.y).lineTo(p2.x, p2.y).stroke({
width: midWidth,
color,
alpha: midAlpha
})
// 内层发光
if (intensity > 0.4) {
const innerWidth = widthDynamic * 0.6
const innerAlpha = alphaDynamic * 0.4
innerGlow.moveTo(p1.x, p1.y).lineTo(p2.x, p2.y).stroke({
width: innerWidth,
color,
alpha: innerAlpha
})
}
}
}
}
// 绘制静态边框
// drawStaticBorder()
// 动画循环
let startTime = performance.now()
const loop = () => {
if (!appRef.current) return
const now = performance.now()
const elapsed = (now - startTime) / 1000
const phase = elapsed * flowSpeed * Math.PI * 2
drawFlowBorder(phase)
rafRef.current = requestAnimationFrame(loop)
}
rafRef.current = requestAnimationFrame(loop)
// 监听尺寸变化 - 监听外层容器而不是 canvas 容器
const handleResize = () => {
if (!appRef.current || !containerRef.current) return
const newWidth = containerRef.current.offsetWidth || 1
const newHeight = containerRef.current.offsetHeight || 1
appRef.current.renderer.resize(newWidth, newHeight)
}
const ro = new ResizeObserver(handleResize)
if (containerRef.current) {
ro.observe(containerRef.current)
}
cleanup = () => {
ro.disconnect()
if (rafRef.current) {
cancelAnimationFrame(rafRef.current)
rafRef.current = null
}
if (appRef.current) {
appRef.current.destroy(true, { children: true })
appRef.current = null
}
if (canvas && el.contains(canvas)) {
el.removeChild(canvas)
}
}
}
initApp().catch(console.error)
return () => {
if (cleanup) cleanup()
}
}, [enabled, borderWidth, borderRadius, glowIntensity, flowSpeed, baseColor, midColor, highlightColor, zIndex])
// 监听 show 的变化,当显示时手动触发一次 resize
useEffect(() => {
if (show && appRef.current && containerRef.current) {
const newWidth = containerRef.current.offsetWidth || 1
const newHeight = containerRef.current.offsetHeight || 1
appRef.current.renderer.resize(newWidth, newHeight)
}
}, [show])
// 始终渲染完整结构,通过 CSS 控制 canvas 容器的可见性
return (
<div
ref={containerRef}
style={{
position: 'relative',
width: '100%',
height: '100%',
border: 'none',
isolation: 'auto'
}}
>
{/* 内容层 - 正常文档流,可交互 */}
{children}
{/* Canvas 容器层 - 绝对定位覆盖层,通过 opacity 控制显示隐藏 */}
<div
ref={canvasContainerRef}
style={{
position: 'absolute',
inset: 0,
pointerEvents: 'none',
opacity: show ? 1 : 0,
transition: 'opacity 0.3s ease',
zIndex: zIndex
}}
/>
</div>
)
}
)
// 添加显示名称以便于调试 GlowBorderWrapper.displayName = 'GlowBorderWrapper'
export default GlowBorderWrapper