pixi边框

13 阅读5分钟

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