@@ -93,12 +93,13 @@ export default function RepoGraph({ repos }: RepoGraphProps) {
9393 renderer . setClearColor ( 0x000000 , 0 )
9494 mount . appendChild ( renderer . domElement )
9595
96- const scene = new THREE . Scene ( )
96+ const scene = new THREE . Scene ( )
9797 const camera = new THREE . PerspectiveCamera ( 50 , w / h , 0.1 , 100 )
98- camera . position . set ( 0 , 2.5 , 10 )
98+ // Camera stays fixed — we rotate the graph group instead
99+ camera . position . set ( 0 , 3 , 12 )
99100 camera . lookAt ( 0 , 0 , 0 )
100101
101- // Lights
102+ // Lights (attached to scene so they don't rotate with the group)
102103 scene . add ( new THREE . AmbientLight ( 0x0d1a3a , 6 ) )
103104 const key = new THREE . PointLight ( 0x3b82f6 , 60 , 28 )
104105 key . position . set ( - 5 , 7 , 9 )
@@ -107,6 +108,13 @@ export default function RepoGraph({ repos }: RepoGraphProps) {
107108 fill . position . set ( 6 , - 3 , 6 )
108109 scene . add ( fill )
109110
111+ // ── Tilted graph group ────────────────────────────────────────────────────
112+ // Tilt ~22° toward the viewer on X so the ring reads as a proper angled plane
113+ const BASE_TILT = - 0.38 // radians on X
114+ const graphGroup = new THREE . Group ( )
115+ graphGroup . rotation . x = BASE_TILT
116+ scene . add ( graphGroup )
117+
110118 // ── Node positions ────────────────────────────────────────────────────────
111119 const featured = repos . filter ( ( r ) => r . featured )
112120 const others = repos . filter ( ( r ) => ! r . featured )
@@ -115,25 +123,26 @@ export default function RepoGraph({ repos }: RepoGraphProps) {
115123
116124 const basePositions : THREE . Vector3 [ ] = [ ]
117125
118- // Featured: tight inner ring
126+ // Featured: tight inner ring with more Y spread for depth
119127 featured . forEach ( ( _ , i ) => {
120128 const angle = ( i / Math . max ( featured . length , 1 ) ) * Math . PI * 2
121129 const r = featured . length === 1 ? 0 : 1.8
122130 basePositions . push ( new THREE . Vector3 (
123131 Math . cos ( angle ) * r ,
124- ( Math . random ( ) - 0.5 ) * 0 .8,
125- Math . sin ( angle ) * r * 0.45 ,
132+ ( Math . random ( ) - 0.5 ) * 1 .8,
133+ Math . sin ( angle ) * r ,
126134 ) )
127135 } )
128136
129- // Others: outer ring(s)
137+ // Others: outer ring with alternating elevation layers for 3-D scatter feel
130138 others . forEach ( ( _ , i ) => {
131139 const angle = ( i / others . length ) * Math . PI * 2 + Math . PI / others . length
132- const r = 3.4 + ( i % 2 ) * 0.75
140+ const r = 3.4 + ( i % 2 ) * 0.8
141+ const layer = ( i % 3 ) - 1 // -1, 0, +1 → three height bands
133142 basePositions . push ( new THREE . Vector3 (
134143 Math . cos ( angle ) * r ,
135- ( Math . random ( ) - 0.5 ) * 1 .6,
136- Math . sin ( angle ) * r * 0.45 ,
144+ layer * 1.4 + ( Math . random ( ) - 0.5 ) * 0 .6,
145+ Math . sin ( angle ) * r ,
137146 ) )
138147 } )
139148
@@ -153,16 +162,17 @@ export default function RepoGraph({ repos }: RepoGraphProps) {
153162 const mesh = new THREE . Mesh ( geo , mat )
154163 mesh . position . copy ( basePositions [ i ] )
155164 mesh . userData = { idx : i }
156- scene . add ( mesh )
165+ graphGroup . add ( mesh )
157166
158167 // Glow halo
159168 const haloGeo = new THREE . SphereGeometry ( radius * 1.75 , 16 , 16 )
160169 const haloMat = new THREE . MeshBasicMaterial ( {
161170 color : col , transparent : true , opacity : 0.07 ,
162171 side : THREE . BackSide , blending : THREE . AdditiveBlending , depthWrite : false ,
163172 } )
164- scene . add ( new THREE . Mesh ( haloGeo , haloMat ) )
165- scene . children [ scene . children . length - 1 ] . position . copy ( basePositions [ i ] )
173+ const halo = new THREE . Mesh ( haloGeo , haloMat )
174+ halo . position . copy ( basePositions [ i ] )
175+ graphGroup . add ( halo )
166176
167177 // Featured ring
168178 if ( repo . featured ) {
@@ -171,29 +181,27 @@ export default function RepoGraph({ repos }: RepoGraphProps) {
171181 const ring = new THREE . Mesh ( ringGeo , ringMat )
172182 ring . position . copy ( basePositions [ i ] )
173183 ring . rotation . x = Math . PI / 2
174- scene . add ( ring )
184+ graphGroup . add ( ring )
175185 }
176186
177187 // Label
178188 const label = makeLabel ( repo . name , repo . stars )
179189 label . position . set ( basePositions [ i ] . x , basePositions [ i ] . y + radius + 0.52 , basePositions [ i ] . z )
180- scene . add ( label )
190+ graphGroup . add ( label )
181191
182192 nodes . push ( { mesh, mat, haloMat, repo, idx : i } )
183193 } )
184194
185195 // ── Connection lines ──────────────────────────────────────────────────────
186196 const lineVerts : number [ ] = [ ]
187197
188- // Same-language connections
189198 for ( let i = 0 ; i < ordered . length ; i ++ ) {
190199 for ( let j = i + 1 ; j < ordered . length ; j ++ ) {
191200 if ( ordered [ i ] . language && ordered [ i ] . language === ordered [ j ] . language ) {
192201 lineVerts . push ( ...basePositions [ i ] . toArray ( ) , ...basePositions [ j ] . toArray ( ) )
193202 }
194203 }
195204 }
196- // Featured → first 3 others
197205 featured . forEach ( ( _ , fi ) => {
198206 others . slice ( 0 , 3 ) . forEach ( ( _ , oi ) => {
199207 lineVerts . push ( ...basePositions [ fi ] . toArray ( ) , ...basePositions [ featured . length + oi ] . toArray ( ) )
@@ -207,10 +215,10 @@ export default function RepoGraph({ repos }: RepoGraphProps) {
207215 color : 0x3b82f6 , transparent : true , opacity : 0.14 ,
208216 blending : THREE . AdditiveBlending , depthWrite : false ,
209217 } )
210- scene . add ( new THREE . LineSegments ( lineGeo , lineMat ) )
218+ graphGroup . add ( new THREE . LineSegments ( lineGeo , lineMat ) )
211219 }
212220
213- // ── Ambient particles ──────────────────────────────────────── ─────────────
221+ // ── Ambient particles (in scene, not group — they stay still) ─────────────
214222 const PC = 70
215223 const pp = new Float32Array ( PC * 3 )
216224 for ( let i = 0 ; i < PC ; i ++ ) {
@@ -222,8 +230,9 @@ export default function RepoGraph({ repos }: RepoGraphProps) {
222230 pGeo . setAttribute ( 'position' , new THREE . BufferAttribute ( pp , 3 ) )
223231 scene . add ( new THREE . Points ( pGeo , new THREE . PointsMaterial ( { color : 0x3b82f6 , size : 0.032 , transparent : true , opacity : 0.35 } ) ) )
224232
225- // ── Orbit / drag ──────────────────────────────────────────────────────────
226- let orbitY = 0 , orbitX = 0
233+ // ── Drag / interaction ────────────────────────────────────────────────────
234+ let rotY = 0 // group Y rotation (auto-spin + drag)
235+ let rotXOffset = 0 // drag offset on top of BASE_TILT
227236 let autoRotate = true
228237 let isDragging = false , prevDX = 0 , prevDY = 0
229238 const mouse = new THREE . Vector2 ( - 99 , - 99 )
@@ -233,8 +242,8 @@ export default function RepoGraph({ repos }: RepoGraphProps) {
233242 const onUp = ( ) => { isDragging = false ; setTimeout ( ( ) => { autoRotate = true } , 2200 ) }
234243 const onMove = ( e : MouseEvent ) => {
235244 if ( isDragging ) {
236- orbitY += ( e . clientX - prevDX ) * 0.005
237- orbitX = Math . max ( - 0.55 , Math . min ( 0.55 , orbitX + ( e . clientY - prevDY ) * 0.003 ) )
245+ rotY += ( e . clientX - prevDX ) * 0.005
246+ rotXOffset = Math . max ( - 0.45 , Math . min ( 0.45 , rotXOffset + ( e . clientY - prevDY ) * 0.003 ) )
238247 prevDX = e . clientX ; prevDY = e . clientY
239248 }
240249 const rect = mount . getBoundingClientRect ( )
@@ -275,13 +284,10 @@ export default function RepoGraph({ repos }: RepoGraphProps) {
275284 frameId = requestAnimationFrame ( animate )
276285 const t = clock . getElapsedTime ( )
277286
278- if ( autoRotate ) orbitY += 0.0025
279-
280- const dist = 10
281- camera . position . x = Math . sin ( orbitY ) * dist
282- camera . position . z = Math . cos ( orbitY ) * dist
283- camera . position . y = 2.5 + orbitX * 5
284- camera . lookAt ( 0 , 0 , 0 )
287+ // Rotate the group (not the camera) for a clean tilted-disk spin
288+ if ( autoRotate ) rotY += 0.0025
289+ graphGroup . rotation . y = rotY
290+ graphGroup . rotation . x = BASE_TILT + rotXOffset
285291
286292 // Throttled hover detection
287293 if ( t - lastHoverCheck > 0.045 ) {
@@ -293,7 +299,10 @@ export default function RepoGraph({ repos }: RepoGraphProps) {
293299 if ( newIdx !== hoveredIdxRef . current ) {
294300 hoveredIdxRef . current = newIdx
295301 if ( newIdx >= 0 ) {
296- const proj = nodes [ newIdx ] . mesh . position . clone ( ) . project ( camera )
302+ // Use world position so the tilt is accounted for in the tooltip
303+ graphGroup . updateMatrixWorld ( )
304+ const worldPos = nodes [ newIdx ] . mesh . getWorldPosition ( new THREE . Vector3 ( ) )
305+ const proj = worldPos . project ( camera )
297306 setTipPos ( {
298307 x : ( ( proj . x + 1 ) / 2 ) * mount . clientWidth ,
299308 y : ( ( - proj . y + 1 ) / 2 ) * mount . clientHeight ,
0 commit comments