feat(mobile): add gyroscope background parallax tilt and tighten hero top spacing

- Add DeviceOrientationEvent tilt to 3D wave mesh, section models, and dot grid on mobile
- Adjust mobile hero layout to eliminate excessive top whitespace below navbar
This commit is contained in:
ssbg04 committed 2026-09-13 14:09:21 +08:00
1 parent 2b729fb213
commit 5a46a00827
2 files changed
+84 -10

No files matched your search

+82 -8
View File
@@ -701,7 +701,7 @@ export default function Background() {
window.addEventListener('resize', handleResize);
handleResize();
// ─── 8. Mouse Parallax Input ───
// ─── 8. Mouse & Mobile Gyroscope Parallax Input ───
let mouseTargetX = 0;
let mouseTargetY = 0;
let currentMouseX = 0;
@@ -718,6 +718,74 @@ export default function Background() {
window.addEventListener('mousemove', handleMouseMove, { passive: true });
// Mobile / Tablet Gyroscope Tilt Handler (DeviceOrientation)
const handleDeviceOrientation = (e: DeviceOrientationEvent) => {
if (e.gamma === null || e.beta === null) return;
// Screen rotation adjustment (portrait vs landscape)
const orientationAngle =
(screen.orientation && screen.orientation.angle) !== undefined
? screen.orientation.angle
: (typeof window.orientation === 'number' ? window.orientation : 0);
let rawX = 0;
let rawY = 0;
if (orientationAngle === 90) {
// Landscape (turned 90deg clockwise)
rawX = -e.beta;
rawY = e.gamma;
} else if (orientationAngle === -90 || orientationAngle === 270) {
// Landscape (turned 90deg counter-clockwise)
rawX = e.beta;
rawY = -e.gamma;
} else if (orientationAngle === 180) {
// Upside down
rawX = -e.gamma;
rawY = -(e.beta - 45);
} else {
// Standard Portrait:
// gamma: left (-)/right (+) roll tilt (-25deg to +25deg)
// beta: front/back pitch tilt (natural resting hand angle is ~45deg)
rawX = e.gamma;
rawY = e.beta - 45;
}
// Smooth normalization based on natural 25-degree hand movement
const maxTilt = 25;
mouseTargetX = Math.min(Math.max(rawX / maxTilt, -1.2), 1.2);
mouseTargetY = Math.min(Math.max(rawY / maxTilt, -1.2), 1.2);
};
let iosPermissionRequested = false;
const requestIOSOrientation = () => {
if (iosPermissionRequested) return;
iosPermissionRequested = true;
if (
typeof DeviceOrientationEvent !== 'undefined' &&
typeof (DeviceOrientationEvent as any).requestPermission === 'function'
) {
(DeviceOrientationEvent as any)
.requestPermission()
.then((state: string) => {
if (state === 'granted') {
window.addEventListener('deviceorientation', handleDeviceOrientation, { passive: true });
}
})
.catch(() => {});
}
};
if (
typeof DeviceOrientationEvent !== 'undefined' &&
typeof (DeviceOrientationEvent as any).requestPermission === 'function'
) {
window.addEventListener('touchend', requestIOSOrientation, { once: true, passive: true });
} else if (typeof window !== 'undefined' && 'DeviceOrientationEvent' in window) {
window.addEventListener('deviceorientation', handleDeviceOrientation, { passive: true });
}
// ─── 9. Render & Parallax Loop ───
let isVisible = true;
@@ -747,21 +815,25 @@ export default function Background() {
terrainProgram.uniforms.uScrollProgress.value = scrollProgress;
// Mouse Parallax Damping
// Mouse & Device Tilt Parallax Damping
currentMouseX += (mouseTargetX - currentMouseX) * 0.05;
currentMouseY += (mouseTargetY - currentMouseY) * 0.05;
// Static Blueprint Dot Grid Parallax
// Static Blueprint Dot Grid Parallax with device tilt
if (gridRef.current && !reducedMotion) {
const gridParallaxY = -(currentScrollY * 0.12);
gridRef.current.style.transform = `translate3d(0, ${gridParallaxY}px, 0)`;
const gridTiltX = currentMouseX * 14;
const gridTiltY = gridParallaxY + currentMouseY * 14;
gridRef.current.style.transform = `translate3d(${gridTiltX.toFixed(1)}px, ${gridTiltY.toFixed(1)}px, 0)`;
}
// Abstract Terrain Parallax Motion (undulates and tilts smoothly)
const targetTerrainY = -0.88 + (scrollProgress * 0.35) - currentMouseY * 0.12;
const targetTerrainRotX = -0.95 + (scrollProgress * 0.2) - currentMouseY * 0.15;
const targetTerrainRotY = 0.06 + currentMouseX * 0.18;
// Abstract Terrain Parallax Motion (undulates and tilts smoothly with mouse or phone tilt)
const targetTerrainY = -0.88 + (scrollProgress * 0.35) - currentMouseY * 0.14;
const targetTerrainRotX = -0.95 + (scrollProgress * 0.2) - currentMouseY * 0.2;
const targetTerrainRotY = 0.06 + currentMouseX * 0.22;
const targetTerrainX = currentMouseX * 0.3;
terrainMesh.position.x += (targetTerrainX - terrainMesh.position.x) * 0.06;
terrainMesh.position.y += (targetTerrainY - terrainMesh.position.y) * 0.06;
terrainMesh.rotation.x += (targetTerrainRotX - terrainMesh.rotation.x) * 0.06;
terrainMesh.rotation.y += (targetTerrainRotY - terrainMesh.rotation.y) * 0.06;
@@ -967,6 +1039,8 @@ export default function Background() {
document.removeEventListener('visibilitychange', handleVisibility);
window.removeEventListener('resize', handleResize);
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('deviceorientation', handleDeviceOrientation);
window.removeEventListener('touchend', requestIOSOrientation);
try {
if (gl.canvas && gl.canvas.parentElement) {
+2 -2
View File
@@ -231,9 +231,9 @@ export default function Hero({
return (
<section
id="home"
className="relative min-h-[calc(100vh-60px)] flex flex-col md:flex-row items-center justify-center pt-6 sm:pt-8 pb-10 md:pb-12 overflow-hidden"
className="relative min-h-[calc(100vh-60px)] flex flex-col md:flex-row items-center justify-start md:justify-center pt-2 sm:pt-4 md:pt-8 pb-10 md:pb-12 overflow-hidden"
>
<div className="container mx-auto px-6 max-w-5xl relative z-10 grid grid-cols-1 md:grid-cols-12 gap-8 md:gap-12 items-center">
<div className="container mx-auto px-6 max-w-5xl relative z-10 grid grid-cols-1 md:grid-cols-12 gap-6 md:gap-12 items-center">
{/* Left Column: Text & Actions */}
<motion.div
className="md:col-span-7 flex flex-col items-center md:items-start text-center md:text-left order-2 md:order-1 will-change-transform-opacity"