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