11import { type RefObject , useEffect } from 'react'
22
3- interface Particle {
3+ type StarType = 'tiny' | 'medium' | 'bright'
4+
5+ interface Star {
46 x : number ; y : number
57 vx : number ; vy : number
68 r : number
7- baseAlpha : number
9+ glowR : number
810 alpha : number
9- pulse : number // phase for opacity pulse
10- pulseSpd : number
11- node : boolean // larger "node" particle
11+ baseAlpha : number
12+ phase : number
13+ phaseSpd : number
14+ cr : number ; cg : number ; cb : number // color components
15+ type : StarType
16+ spikes : boolean
1217}
1318
14- // Dark theme colors
15- const D_ACCENT : [ number , number , number ] = [ 56 , 189 , 248 ]
16- const D_ACCENT2 : [ number , number , number ] = [ 129 , 140 , 248 ]
17- const D_WHITE : [ number , number , number ] = [ 255 , 255 , 255 ]
18- // Light theme colors (visible on light bg)
19- const L_ACCENT : [ number , number , number ] = [ 2 , 132 , 199 ]
20- const L_ACCENT2 : [ number , number , number ] = [ 79 , 70 , 229 ]
21- const L_DARK : [ number , number , number ] = [ 15 , 23 , 42 ]
19+ // Star color palette — white, blue-white, warm-white, cyan-white, lavender
20+ const COLORS : [ number , number , number ] [ ] = [
21+ [ 255 , 255 , 255 ] ,
22+ [ 210 , 230 , 255 ] ,
23+ [ 180 , 210 , 255 ] ,
24+ [ 255 , 248 , 210 ] ,
25+ [ 160 , 225 , 255 ] ,
26+ [ 220 , 205 , 255 ] ,
27+ [ 200 , 240 , 255 ] ,
28+ ]
2229
2330export function useStarfield ( canvasRef : RefObject < HTMLCanvasElement | null > ) {
2431 useEffect ( ( ) => {
@@ -30,12 +37,10 @@ export function useStarfield(canvasRef: RefObject<HTMLCanvasElement | null>) {
3037 const rnd = ( a : number , b : number ) => a + Math . random ( ) * ( b - a )
3138
3239 let W = 0 , H = 0
33- let particles : Particle [ ] = [ ]
40+ let stars : Star [ ] = [ ]
3441 let t = 0 , rafId = 0 , paused = false
3542
36- // How many particles based on screen area
37- const count = ( ) => Math . min ( Math . floor ( ( window . innerWidth * window . innerHeight ) / 5500 ) , 280 )
38- const CONNECT_DIST = 180 // px — max distance to draw a line
43+ const count = ( ) => Math . min ( Math . floor ( ( window . innerWidth * window . innerHeight ) / 2800 ) , 520 )
3944 const isLight = ( ) => document . documentElement . dataset . theme === 'light'
4045
4146 function resize ( ) {
@@ -48,101 +53,119 @@ export function useStarfield(canvasRef: RefObject<HTMLCanvasElement | null>) {
4853 ctx . scale ( dpr , dpr )
4954 }
5055
51- function mkParticle ( ) : Particle {
52- const isNode = Math . random ( ) < 0.12 // ~12% are larger nodes
56+ function mkStar ( ) : Star {
57+ const roll = Math . random ( )
58+ const type : StarType = roll < 0.12 ? 'bright' : roll < 0.38 ? 'medium' : 'tiny'
59+ const [ cr , cg , cb ] = COLORS [ Math . floor ( Math . random ( ) * COLORS . length ) ]
60+
61+ const r = type === 'bright' ? rnd ( 1.6 , 2.8 ) : type === 'medium' ? rnd ( 0.8 , 1.5 ) : rnd ( 0.25 , 0.75 )
62+ const speed = type === 'bright' ? 0.04 : type === 'medium' ? 0.07 : 0.12
63+
5364 return {
5465 x : rnd ( 0 , W / dpr ) ,
5566 y : rnd ( 0 , H / dpr ) ,
56- vx : rnd ( - 0.5 , 0.5 ) ,
57- vy : rnd ( - 0.4 , 0.4 ) ,
58- r : isNode ? rnd ( 2.2 , 3.8 ) : rnd ( 0.8 , 1.8 ) ,
59- baseAlpha : isNode ? rnd ( 0.6 , 0.9 ) : rnd ( 0.25 , 0.65 ) ,
67+ vx : rnd ( - speed , speed ) ,
68+ vy : rnd ( - speed * 0.8 , speed * 0.8 ) ,
69+ r,
70+ glowR : type === 'bright' ? rnd ( 10 , 20 ) : rnd ( 3 , 7 ) ,
71+ baseAlpha : type === 'bright' ? rnd ( 0.75 , 1.0 ) : type === 'medium' ? rnd ( 0.4 , 0.75 ) : rnd ( 0.15 , 0.55 ) ,
6072 alpha : 0 ,
61- pulse : rnd ( 0 , PI2 ) ,
62- pulseSpd : rnd ( 0.006 , 0.018 ) ,
63- node : isNode ,
73+ phase : rnd ( 0 , PI2 ) ,
74+ phaseSpd : type === 'bright' ? rnd ( 0.004 , 0.012 ) : rnd ( 0.01 , 0.03 ) ,
75+ cr, cg, cb,
76+ type,
77+ spikes : type === 'bright' && Math . random ( ) < 0.65 ,
6478 }
6579 }
6680
6781 function init ( ) {
68- particles = Array . from ( { length : count ( ) } , mkParticle )
82+ stars = Array . from ( { length : count ( ) } , mkStar )
6983 }
7084
71- // Pick a color for a particle — theme-aware
72- function pickColor ( p : Particle ) : [ number , number , number ] {
73- const h = p . x / ( W / dpr ) // positional tint
74- if ( isLight ( ) ) {
75- if ( h < 0.33 ) return L_ACCENT
76- if ( h < 0.66 ) return L_DARK
77- return L_ACCENT2
85+ function drawSpikes ( x : number , y : number , r : number , cr : number , cg : number , cb : number , a : number ) {
86+ const len = r * 14
87+ const lw = r * 0.7
88+ for ( let i = 0 ; i < 4 ; i ++ ) {
89+ const angle = ( i * Math . PI ) / 2
90+ const ex = x + Math . cos ( angle ) * len
91+ const ey = y + Math . sin ( angle ) * len
92+ const grd = ctx . createLinearGradient ( x , y , ex , ey )
93+ grd . addColorStop ( 0 , `rgba(${ cr } ,${ cg } ,${ cb } ,${ a * 0.75 } )` )
94+ grd . addColorStop ( 0.4 , `rgba(${ cr } ,${ cg } ,${ cb } ,${ a * 0.2 } )` )
95+ grd . addColorStop ( 1 , `rgba(${ cr } ,${ cg } ,${ cb } ,0)` )
96+ ctx . beginPath ( )
97+ ctx . moveTo ( x , y )
98+ ctx . lineTo ( ex , ey )
99+ ctx . strokeStyle = grd
100+ ctx . lineWidth = lw
101+ ctx . stroke ( )
78102 }
79- if ( h < 0.33 ) return D_ACCENT
80- if ( h < 0.66 ) return D_WHITE
81- return D_ACCENT2
82103 }
83104
84105 function draw ( ) {
85106 const W2 = W / dpr , H2 = H / dpr
86107 ctx . clearRect ( 0 , 0 , W2 , H2 )
87108 t ++
88109
89- // update positions
90- particles . forEach ( p => {
91- p . x += p . vx
92- p . y += p . vy
93-
94- // wrap around edges with a soft margin
95- if ( p . x < - 20 ) p . x = W2 + 20
96- if ( p . x > W2 + 20 ) p . x = - 20
97- if ( p . y < - 20 ) p . y = H2 + 20
98- if ( p . y > H2 + 20 ) p . y = - 20
99-
100- p . alpha = p . baseAlpha * ( 0.55 + 0.45 * Math . sin ( t * p . pulseSpd + p . pulse ) )
101- } )
102-
103- // draw connections
104- for ( let i = 0 ; i < particles . length ; i ++ ) {
105- const a = particles [ i ]
106- for ( let j = i + 1 ; j < particles . length ; j ++ ) {
107- const b = particles [ j ]
108- const dx = a . x - b . x
109- const dy = a . y - b . y
110- const dist = Math . sqrt ( dx * dx + dy * dy )
111- if ( dist > CONNECT_DIST ) continue
112-
113- const lineAlpha = ( 1 - dist / CONNECT_DIST ) * 0.18 * Math . min ( a . alpha , b . alpha ) * 2
114- const [ r , g , bv ] = pickColor ( a )
115- ctx . beginPath ( )
116- ctx . moveTo ( a . x , a . y )
117- ctx . lineTo ( b . x , b . y )
118- ctx . strokeStyle = `rgba(${ r } ,${ g } ,${ bv } ,${ lineAlpha } )`
119- ctx . lineWidth = 0.6
120- ctx . stroke ( )
121- }
122- }
110+ const lightMode = isLight ( )
123111
124- // draw particles
125- particles . forEach ( p => {
126- const [ r , g , b ] = pickColor ( p )
127- const a = p . alpha
112+ for ( const s of stars ) {
113+ // drift
114+ s . x += s . vx
115+ s . y += s . vy
116+ if ( s . x < - 15 ) s . x = W2 + 15
117+ if ( s . x > W2 + 15 ) s . x = - 15
118+ if ( s . y < - 15 ) s . y = H2 + 15
119+ if ( s . y > H2 + 15 ) s . y = - 15
128120
129- // glow halo for nodes
130- if ( p . node ) {
131- const grd = ctx . createRadialGradient ( p . x , p . y , 0 , p . x , p . y , p . r * 6 )
132- grd . addColorStop ( 0 , `rgba(${ r } ,${ g } ,${ b } ,${ a * 0.25 } )` )
133- grd . addColorStop ( 1 , `rgba(${ r } ,${ g } ,${ b } ,0)` )
121+ // twinkle
122+ const tw = 0.35 + 0.65 * ( 0.5 + 0.5 * Math . sin ( t * s . phaseSpd + s . phase ) )
123+ // light mode: reduce visibility significantly
124+ const alphaScale = lightMode ? 0.25 : 1
125+ s . alpha = s . baseAlpha * tw * alphaScale
126+
127+ const { cr, cg, cb } = s
128+ const a = s . alpha
129+
130+ if ( s . type === 'tiny' ) {
131+ // simple dot only — no gradient cost
134132 ctx . beginPath ( )
135- ctx . arc ( p . x , p . y , p . r * 6 , 0 , PI2 )
136- ctx . fillStyle = grd
133+ ctx . arc ( s . x , s . y , s . r , 0 , PI2 )
134+ ctx . fillStyle = `rgba( ${ cr } , ${ cg } , ${ cb } , ${ a } )`
137135 ctx . fill ( )
136+ continue
138137 }
139138
140- // core dot
139+ // outer glow
140+ const grd = ctx . createRadialGradient ( s . x , s . y , 0 , s . x , s . y , s . glowR )
141+ if ( s . type === 'bright' ) {
142+ grd . addColorStop ( 0 , `rgba(${ cr } ,${ cg } ,${ cb } ,${ a * 0.35 } )` )
143+ grd . addColorStop ( 0.3 , `rgba(${ cr } ,${ cg } ,${ cb } ,${ a * 0.12 } )` )
144+ grd . addColorStop ( 1 , `rgba(${ cr } ,${ cg } ,${ cb } ,0)` )
145+ } else {
146+ grd . addColorStop ( 0 , `rgba(${ cr } ,${ cg } ,${ cb } ,${ a * 0.2 } )` )
147+ grd . addColorStop ( 1 , `rgba(${ cr } ,${ cg } ,${ cb } ,0)` )
148+ }
141149 ctx . beginPath ( )
142- ctx . arc ( p . x , p . y , p . r , 0 , PI2 )
143- ctx . fillStyle = `rgba( ${ r } , ${ g } , ${ b } , ${ a } )`
150+ ctx . arc ( s . x , s . y , s . glowR , 0 , PI2 )
151+ ctx . fillStyle = grd
144152 ctx . fill ( )
145- } )
153+
154+ // spikes for bright stars
155+ if ( s . spikes ) {
156+ drawSpikes ( s . x , s . y , s . r , cr , cg , cb , a )
157+ }
158+
159+ // bright core with radial gradient
160+ const core = ctx . createRadialGradient ( s . x , s . y , 0 , s . x , s . y , s . r * 2 )
161+ core . addColorStop ( 0 , `rgba(${ cr } ,${ cg } ,${ cb } ,${ a } )` )
162+ core . addColorStop ( 0.4 , `rgba(${ cr } ,${ cg } ,${ cb } ,${ a * 0.8 } )` )
163+ core . addColorStop ( 1 , `rgba(${ cr } ,${ cg } ,${ cb } ,0)` )
164+ ctx . beginPath ( )
165+ ctx . arc ( s . x , s . y , s . r * 2 , 0 , PI2 )
166+ ctx . fillStyle = core
167+ ctx . fill ( )
168+ }
146169 }
147170
148171 function loop ( ) {
0 commit comments