Skip to content

Commit 8a6c73b

Browse files
committed
Mobile responsive + floating parts + robot improvements
- Responsive viewport scaling for all screen sizes - Dynamic FOV and camera positions based on viewport width - Floating robot parts (bolts, nuts, gears) with hover/tap interaction - Seamless robot arm joints with detail accents - Tighter contact orb layout on mobile - Smaller ghost stack cards on mobile
1 parent 0b5bb2b commit 8a6c73b

File tree

10 files changed

+336
-71
lines changed

10 files changed

+336
-71
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ dist
33
.DS_Store
44
*.local
55
assets/cv/
6+
.*/

src/components/canvas/CameraRig.jsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,13 @@ export default function CameraRig({ mouseParallax, hoveredSection, openSection,
7777

7878
camera.position.copy(_smoothPos)
7979
camera.lookAt(_smoothLook)
80+
81+
// Dynamically update FOV based on viewport
82+
const targetFov = 65 - t * 15 // 65 on mobile, 50 on desktop
83+
if (Math.abs(camera.fov - targetFov) > 0.5) {
84+
camera.fov = THREE.MathUtils.lerp(camera.fov, targetFov, 0.05)
85+
camera.updateProjectionMatrix()
86+
}
8087
})
8188

8289
return null

src/components/canvas/Robot.jsx

Lines changed: 51 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export default function Robot({ position = [0, 0, 0], mouseParallax, hoveredSect
5454
const blue = { color: '#2563eb', metalness: 0.08, roughness: 0.5 }
5555

5656
return (
57-
<group position={position} scale={2.5 + vs.vw * 2}>
57+
<group position={position} scale={3.8 + vs.vw * 0.7}>
5858

5959
{/* ════════════ BODY ════════════ */}
6060
{/* Main torso — rounded, Astro Bot style */}
@@ -213,43 +213,71 @@ export default function Robot({ position = [0, 0, 0], mouseParallax, hoveredSect
213213
))}
214214
</group>
215215

216-
{/* ════════════ SHOULDERS (blue accents) ════════════ */}
216+
{/* ════════════ ARMS (seamless joints) ════════════ */}
217217
{[-1, 1].map((side, i) => (
218-
<group key={i} position={[side * 0.52, 0.2, 0]}>
219-
<mesh>
218+
<group key={i} position={[side * 0.5, 0.15, 0]} rotation={[0, 0, side * 0.12]}>
219+
{/* Shoulder ball — overlaps with body */}
220+
<mesh position={[side * 0.04, 0.02, 0]}>
220221
<sphereGeometry args={[0.1, 16, 16]} />
221-
<meshStandardMaterial {...dark} />
222+
<meshStandardMaterial {...shiny} />
222223
</mesh>
223-
<mesh position={[side * 0.02, 0, 0]}>
224-
<sphereGeometry args={[0.08, 12, 12]} />
224+
{/* Shoulder accent ring */}
225+
<mesh position={[side * 0.04, 0.02, 0]} rotation={[0, 0, side * 0.2]}>
226+
<torusGeometry args={[0.08, 0.008, 12, 24]} rotation={[Math.PI / 2, 0, 0]} />
225227
<meshStandardMaterial {...blue} />
226228
</mesh>
227-
</group>
228-
))}
229229

230-
{/* ════════════ ARMS (static, resting at sides) ════════════ */}
231-
{[-1, 1].map((side, i) => (
232-
<group key={i} position={[side * 0.55, 0.1, 0]} rotation={[0, 0, side * 0.15]}>
233-
{/* Upper arm */}
234-
<mesh position={[side * 0.05, -0.18, 0]}>
235-
<capsuleGeometry args={[0.06, 0.2, 8, 16]} />
230+
{/* Upper arm — overlaps shoulder ball */}
231+
<mesh position={[side * 0.04, -0.14, 0]}>
232+
<capsuleGeometry args={[0.065, 0.18, 8, 16]} />
236233
<meshStandardMaterial {...shiny} />
237234
</mesh>
238-
{/* Elbow */}
239-
<mesh position={[side * 0.05, -0.35, 0]}>
240-
<sphereGeometry args={[0.055, 12, 12]} />
235+
{/* Upper arm detail strip */}
236+
<mesh position={[side * 0.04, -0.14, 0.06]}>
237+
<boxGeometry args={[0.03, 0.16, 0.01]} />
238+
<meshStandardMaterial {...blue} />
239+
</mesh>
240+
241+
{/* Elbow joint — overlaps both arm segments */}
242+
<mesh position={[side * 0.04, -0.3, 0]}>
243+
<sphereGeometry args={[0.065, 16, 16]} />
241244
<meshStandardMaterial {...dark} />
242245
</mesh>
243-
{/* Forearm */}
244-
<mesh position={[side * 0.05, -0.52, 0]}>
245-
<capsuleGeometry args={[0.05, 0.18, 8, 16]} />
246+
{/* Elbow accent */}
247+
<mesh position={[side * 0.04, -0.3, 0]}>
248+
<torusGeometry args={[0.05, 0.006, 8, 16]} rotation={[Math.PI / 2, 0, 0]} />
249+
<meshStandardMaterial color="#38bdf8" emissive="#38bdf8" emissiveIntensity={0.4} toneMapped={false} />
250+
</mesh>
251+
252+
{/* Forearm — overlaps elbow */}
253+
<mesh position={[side * 0.04, -0.46, 0]}>
254+
<capsuleGeometry args={[0.055, 0.2, 8, 16]} />
246255
<meshStandardMaterial {...shiny} />
247256
</mesh>
248-
{/* Hand */}
249-
<mesh position={[side * 0.05, -0.68, 0]}>
257+
{/* Forearm detail strip */}
258+
<mesh position={[side * 0.04, -0.46, 0.05]}>
259+
<boxGeometry args={[0.025, 0.15, 0.01]} />
260+
<meshStandardMaterial {...dark} />
261+
</mesh>
262+
263+
{/* Wrist joint */}
264+
<mesh position={[side * 0.04, -0.6, 0]}>
250265
<sphereGeometry args={[0.05, 12, 12]} />
266+
<meshStandardMaterial {...dark} />
267+
</mesh>
268+
269+
{/* Hand */}
270+
<mesh position={[side * 0.04, -0.68, 0]}>
271+
<sphereGeometry args={[0.055, 16, 16]} />
251272
<meshStandardMaterial {...blue} />
252273
</mesh>
274+
{/* Finger hints */}
275+
{[-1, 0, 1].map((f, fi) => (
276+
<mesh key={fi} position={[side * 0.04 + f * 0.02, -0.74, 0.02]}>
277+
<capsuleGeometry args={[0.012, 0.03, 4, 8]} />
278+
<meshStandardMaterial {...blue} />
279+
</mesh>
280+
))}
253281
</group>
254282
))}
255283

src/components/canvas/Scene.jsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import Robot from './Robot'
77
import GearCluster from '../decorations/GearCluster'
88
import CircuitLines from '../decorations/CircuitLines'
99
import EnergyRing from '../decorations/EnergyRing'
10+
import FloatingParts from '../decorations/FloatingParts'
1011
import IntroSection from '../sections/IntroSection'
1112
import ExperienceSection from '../sections/ExperienceSection'
1213
import EducationSection from '../sections/EducationSection'
@@ -34,7 +35,7 @@ export default function Scene({ mouseParallax, hoveredSection, openSection, togg
3435
<CameraRig mouseParallax={mouseParallax} hoveredSection={hoveredSection} openSection={openSection} vs={vs} />
3536

3637
<Robot
37-
position={[0, lerp(-2, -3.5, t), lerp(3, 1, t)]}
38+
position={[0, lerp(-2.8, -3.5, t), lerp(3, 1, t)]}
3839
mouseParallax={mouseParallax}
3940
hoveredSection={hoveredSection}
4041
vs={vs}
@@ -55,6 +56,8 @@ export default function Scene({ mouseParallax, hoveredSection, openSection, togg
5556
<EnergyRing position={[0, lerp(1.2, 1.5, t), lerp(8, 7, t)]} radius={lerp(0.5, 0.9, t)} color="#a78bfa" speed={0.2} />
5657
)}
5758

59+
<FloatingParts count={15} isMobile={t < 0.5} />
60+
5861
<Suspense fallback={null}>
5962
<IntroSection vs={vs} />
6063
</Suspense>
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
import { useRef, useMemo } from 'react'
2+
import { useFrame } from '@react-three/fiber'
3+
import * as THREE from 'three'
4+
5+
const PART_TYPES = ['bolt', 'nut', 'screw', 'gear', 'spring']
6+
7+
function randomInRange(min, max) {
8+
return min + Math.random() * (max - min)
9+
}
10+
11+
function BoltPart({ color }) {
12+
return (
13+
<group>
14+
<mesh>
15+
<cylinderGeometry args={[0.03, 0.03, 0.12, 6]} />
16+
<meshStandardMaterial color={color} metalness={0.7} roughness={0.3} />
17+
</mesh>
18+
<mesh position={[0, 0.08, 0]}>
19+
<cylinderGeometry args={[0.05, 0.05, 0.04, 6]} />
20+
<meshStandardMaterial color={color} metalness={0.7} roughness={0.3} />
21+
</mesh>
22+
</group>
23+
)
24+
}
25+
26+
function NutPart({ color }) {
27+
return (
28+
<mesh>
29+
<torusGeometry args={[0.07, 0.025, 6, 6]} />
30+
<meshStandardMaterial color={color} metalness={0.7} roughness={0.3} />
31+
</mesh>
32+
)
33+
}
34+
35+
function ScrewPart({ color }) {
36+
return (
37+
<group>
38+
<mesh>
39+
<cylinderGeometry args={[0.03, 0.015, 0.2, 8]} />
40+
<meshStandardMaterial color={color} metalness={0.7} roughness={0.3} />
41+
</mesh>
42+
<mesh position={[0, 0.12, 0]}>
43+
<sphereGeometry args={[0.045, 8, 8]} />
44+
<meshStandardMaterial color={color} metalness={0.7} roughness={0.3} />
45+
</mesh>
46+
</group>
47+
)
48+
}
49+
50+
function GearPart({ color }) {
51+
return (
52+
<mesh>
53+
<torusGeometry args={[0.08, 0.02, 4, 8]} />
54+
<meshStandardMaterial color={color} metalness={0.8} roughness={0.2} />
55+
</mesh>
56+
)
57+
}
58+
59+
function SpringPart({ color }) {
60+
return (
61+
<mesh>
62+
<torusGeometry args={[0.06, 0.015, 8, 16]} />
63+
<meshStandardMaterial color={color} metalness={0.6} roughness={0.3} />
64+
</mesh>
65+
)
66+
}
67+
68+
const PART_COMPONENTS = { bolt: BoltPart, nut: NutPart, screw: ScrewPart, gear: GearPart, spring: SpringPart }
69+
70+
function FloatingPart({ type, startPos, bounds }) {
71+
const groupRef = useRef()
72+
const velocity = useRef(new THREE.Vector3(
73+
randomInRange(-0.003, 0.003),
74+
randomInRange(-0.002, 0.002),
75+
randomInRange(-0.002, 0.002)
76+
))
77+
const rotSpeed = useRef(new THREE.Vector3(
78+
randomInRange(-0.02, 0.02),
79+
randomInRange(-0.02, 0.02),
80+
randomInRange(-0.02, 0.02)
81+
))
82+
const fleeing = useRef(false)
83+
const fleeVel = useRef(new THREE.Vector3())
84+
const fleeTimer = useRef(0)
85+
86+
const PartComponent = PART_COMPONENTS[type]
87+
const color = useMemo(() => {
88+
const colors = ['#a1a1aa', '#b4b4bc', '#c0c0c8', '#d4d4d8', '#9898a0']
89+
return colors[Math.floor(Math.random() * colors.length)]
90+
}, [])
91+
92+
useFrame((_, delta) => {
93+
if (!groupRef.current) return
94+
const pos = groupRef.current.position
95+
96+
// Flee decay
97+
if (fleeing.current) {
98+
fleeTimer.current -= delta
99+
if (fleeTimer.current <= 0) {
100+
fleeing.current = false
101+
} else {
102+
// Apply flee velocity with decay
103+
const decay = fleeTimer.current / 0.8
104+
pos.x += fleeVel.current.x * decay * delta * 60
105+
pos.y += fleeVel.current.y * decay * delta * 60
106+
pos.z += fleeVel.current.z * decay * delta * 60
107+
}
108+
}
109+
110+
// Normal drift
111+
pos.x += velocity.current.x
112+
pos.y += velocity.current.y
113+
pos.z += velocity.current.z
114+
115+
// Bounce off bounds
116+
if (pos.x < bounds.minX || pos.x > bounds.maxX) {
117+
velocity.current.x *= -1
118+
pos.x = THREE.MathUtils.clamp(pos.x, bounds.minX, bounds.maxX)
119+
}
120+
if (pos.y < bounds.minY || pos.y > bounds.maxY) {
121+
velocity.current.y *= -1
122+
pos.y = THREE.MathUtils.clamp(pos.y, bounds.minY, bounds.maxY)
123+
}
124+
if (pos.z < bounds.minZ || pos.z > bounds.maxZ) {
125+
velocity.current.z *= -1
126+
pos.z = THREE.MathUtils.clamp(pos.z, bounds.minZ, bounds.maxZ)
127+
}
128+
129+
// Rotation
130+
groupRef.current.rotation.x += rotSpeed.current.x
131+
groupRef.current.rotation.y += rotSpeed.current.y
132+
groupRef.current.rotation.z += rotSpeed.current.z
133+
})
134+
135+
function onInteract(e) {
136+
e.stopPropagation()
137+
if (!groupRef.current) return
138+
139+
// Calculate flee direction — away from pointer
140+
const pos = groupRef.current.position
141+
const dir = new THREE.Vector3()
142+
dir.copy(pos).sub(e.point).normalize()
143+
144+
// Add some randomness
145+
dir.x += randomInRange(-0.3, 0.3)
146+
dir.y += randomInRange(-0.2, 0.2)
147+
dir.z += randomInRange(-0.2, 0.2)
148+
dir.normalize()
149+
150+
fleeVel.current.copy(dir).multiplyScalar(0.08)
151+
fleeing.current = true
152+
fleeTimer.current = 0.8
153+
154+
// Also change drift direction
155+
velocity.current.set(
156+
randomInRange(-0.004, 0.004),
157+
randomInRange(-0.003, 0.003),
158+
randomInRange(-0.003, 0.003)
159+
)
160+
}
161+
162+
return (
163+
<group
164+
ref={groupRef}
165+
position={startPos}
166+
onPointerEnter={onInteract}
167+
onClick={onInteract}
168+
>
169+
<PartComponent color={color} />
170+
{/* Invisible hitbox for easier interaction */}
171+
<mesh visible={false}>
172+
<sphereGeometry args={[0.12, 8, 8]} />
173+
<meshBasicMaterial />
174+
</mesh>
175+
</group>
176+
)
177+
}
178+
179+
export default function FloatingParts({ count = 15, isMobile }) {
180+
const parts = useMemo(() => {
181+
const n = isMobile ? Math.floor(count * 0.6) : count
182+
return Array.from({ length: n }).map((_, i) => ({
183+
id: i,
184+
type: PART_TYPES[i % PART_TYPES.length],
185+
startPos: [
186+
randomInRange(isMobile ? -3 : -6, isMobile ? 3 : 6),
187+
randomInRange(0.5, 4.5),
188+
randomInRange(0, 9),
189+
],
190+
}))
191+
}, [count, isMobile])
192+
193+
const bounds = useMemo(() => ({
194+
minX: isMobile ? -3 : -7,
195+
maxX: isMobile ? 3 : 7,
196+
minY: 0,
197+
maxY: 5,
198+
minZ: 0,
199+
maxZ: 10,
200+
}), [isMobile])
201+
202+
return (
203+
<group>
204+
{parts.map((p) => (
205+
<FloatingPart
206+
key={p.id}
207+
type={p.type}
208+
startPos={p.startPos}
209+
bounds={bounds}
210+
/>
211+
))}
212+
</group>
213+
)
214+
}

0 commit comments

Comments
 (0)