Skip to content

Commit 7895233

Browse files
committed
Refactor RepoGraph for improved 3D visualization: adjust camera position, introduce graph group for rotation, and enhance node positioning for depth perception
1 parent b345773 commit 7895233

File tree

1 file changed

+40
-31
lines changed

1 file changed

+40
-31
lines changed

src/templates/threejs/RepoGraph.tsx

Lines changed: 40 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)