index.tsx raw

   1  import { useEffect, useRef } from 'react'
   2  import { useTheme } from '@/providers/ThemeProvider'
   3  
   4  const SVG_NS = 'http://www.w3.org/2000/svg'
   5  
   6  interface BranchEdge {
   7    x1: number
   8    y1: number
   9    x2: number
  10    y2: number
  11    color: string
  12    width: number
  13    delay: number
  14  }
  15  
  16  const DARK_COLORS = ['#e07030', '#8833bb', '#00aabb']
  17  const LIGHT_COLORS = ['#2266cc', '#cc2233', '#22aa44']
  18  
  19  const BASE_LEN = 110
  20  const DECAY = 0.56
  21  const BASE_WIDTH = 32
  22  const DEPTH = 6
  23  const SPREAD = Math.PI * 0.68
  24  const CX = 400
  25  const CY = 400
  26  
  27  const BRANCH_ANGLES = [
  28    -Math.PI / 2,
  29    -Math.PI / 2 + (2 * Math.PI) / 3,
  30    -Math.PI / 2 + (4 * Math.PI) / 3,
  31  ]
  32  
  33  function buildEdges(colors: string[]): BranchEdge[] {
  34    const edges: BranchEdge[] = []
  35    let delay = 50
  36  
  37    function buildBranch(
  38      px: number,
  39      py: number,
  40      angle: number,
  41      depth: number,
  42      maxDepth: number,
  43      branchIdx: number,
  44      spreadAngle: number
  45    ) {
  46      if (depth > maxDepth) return
  47  
  48      const scale = Math.pow(DECAY, depth - 1)
  49      const len = BASE_LEN * scale
  50      const width = BASE_WIDTH * scale
  51      const nx = px + Math.cos(angle) * len
  52      const ny = py + Math.sin(angle) * len
  53  
  54      const d = delay
  55      delay += 30
  56  
  57      edges.push({ x1: px, y1: py, x2: nx, y2: ny, color: colors[branchIdx], width, delay: d })
  58  
  59      const childSpread = spreadAngle * 0.82
  60      const offsets = [-childSpread / 2, childSpread / 2]
  61  
  62      for (const off of offsets) {
  63        buildBranch(nx, ny, angle + off, depth + 1, maxDepth, branchIdx, childSpread)
  64      }
  65    }
  66  
  67    for (let i = 0; i < 3; i++) {
  68      buildBranch(CX, CY, BRANCH_ANGLES[i], 1, DEPTH, i, SPREAD)
  69    }
  70  
  71    return edges
  72  }
  73  
  74  function renderSVG(container: SVGGElement, isDark: boolean) {
  75    while (container.firstChild) container.removeChild(container.firstChild)
  76  
  77    const colors = isDark ? DARK_COLORS : LIGHT_COLORS
  78    const edges = buildEdges(colors)
  79  
  80    for (const e of edges) {
  81      const line = document.createElementNS(SVG_NS, 'line')
  82      line.setAttribute('x1', String(e.x1))
  83      line.setAttribute('y1', String(e.y1))
  84      line.setAttribute('x2', String(e.x2))
  85      line.setAttribute('y2', String(e.y2))
  86      line.setAttribute('stroke', e.color)
  87      line.setAttribute('stroke-width', String(e.width))
  88      line.classList.add('smesh-loader-edge')
  89      line.style.animationDelay = e.delay + 'ms'
  90      container.appendChild(line)
  91    }
  92  
  93    // Center hexagon
  94    const r = 24
  95    const hexPoints: string[] = []
  96    for (let i = 0; i < 6; i++) {
  97      const a = Math.PI / 6 + (i * Math.PI) / 3
  98      hexPoints.push(`${(CX + r * Math.cos(a)).toFixed(2)},${(CY + r * Math.sin(a)).toFixed(2)}`)
  99    }
 100    const hex = document.createElementNS(SVG_NS, 'polygon')
 101    hex.setAttribute('points', hexPoints.join(' '))
 102    hex.setAttribute('fill', isDark ? '#e8e4da' : '#1a1a1e')
 103    hex.setAttribute('stroke', isDark ? '#0a0a0e' : '#f5f5f0')
 104    hex.setAttribute('stroke-width', '7.5')
 105    hex.setAttribute('stroke-linejoin', 'round')
 106    hex.classList.add('smesh-loader-center')
 107    hex.style.animationDelay = '0ms'
 108    container.appendChild(hex)
 109  }
 110  
 111  export default function SmeshLoader({ className }: { className?: string }) {
 112    const { theme } = useTheme()
 113    const gRef = useRef<SVGGElement>(null)
 114    const isDark = theme !== 'light'
 115  
 116    useEffect(() => {
 117      if (gRef.current) {
 118        renderSVG(gRef.current, isDark)
 119      }
 120    }, [isDark])
 121  
 122    return (
 123      <div className={className}>
 124        <style>{`
 125          .smesh-loader-edge {
 126            stroke-linecap: round;
 127            opacity: 0;
 128            animation: smeshEdgeFade 0.4s ease forwards;
 129          }
 130          .smesh-loader-center {
 131            opacity: 0;
 132            animation: smeshNodePop 0.3s ease forwards;
 133          }
 134          @keyframes smeshEdgeFade {
 135            to { opacity: 1; }
 136          }
 137          @keyframes smeshNodePop {
 138            0% { opacity: 0; transform: scale(0); }
 139            70% { transform: scale(1.2); }
 140            100% { opacity: 1; transform: scale(1); }
 141          }
 142        `}</style>
 143        <svg viewBox="160.68 160.68 478.65 478.65" className="w-full h-full">
 144          <g ref={gRef} />
 145        </svg>
 146      </div>
 147    )
 148  }
 149