feat: add 3D scooter model and landing page layout enhancements
This commit is contained in:
@@ -1,65 +1,192 @@
|
||||
'use client';
|
||||
import { useRef } from 'react';
|
||||
import { Canvas, useFrame } from '@react-three/fiber';
|
||||
import { Float, Box, Cylinder } from '@react-three/drei';
|
||||
import * as THREE from 'three';
|
||||
|
||||
function StylizedScooter() {
|
||||
import { Suspense, useMemo, useRef } from 'react';
|
||||
import { Canvas, useFrame, useLoader } from '@react-three/fiber';
|
||||
import {
|
||||
Float,
|
||||
ContactShadows,
|
||||
Environment,
|
||||
Lightformer,
|
||||
Sparkles,
|
||||
Html,
|
||||
useProgress,
|
||||
} from '@react-three/drei';
|
||||
import * as THREE from 'three';
|
||||
import { OBJLoader } from 'three-stdlib';
|
||||
|
||||
const ACCENT = '#00bc84';
|
||||
const ACCENT_BRIGHT = '#10b981';
|
||||
|
||||
function Loader() {
|
||||
const { progress } = useProgress();
|
||||
return (
|
||||
<Html center>
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<div className="w-12 h-12 border-4 border-emerald-500/30 border-t-emerald-500 rounded-full animate-spin" />
|
||||
<p className="text-emerald-400 text-sm font-semibold">{Math.round(progress)}%</p>
|
||||
</div>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
|
||||
function AeroxModel() {
|
||||
const obj = useLoader(OBJLoader, '/uploads_files_3634924_uploads_files_850048_aerox.obj');
|
||||
useLoader.preload(OBJLoader, '/uploads_files_3634924_uploads_files_850048_aerox.obj');
|
||||
const group = useRef<THREE.Group>(null);
|
||||
|
||||
useFrame((state, delta) => {
|
||||
if (group.current) {
|
||||
group.current.rotation.y += delta * 0.2;
|
||||
group.current.rotation.z = Math.sin(state.clock.elapsedTime * 0.5) * 0.05;
|
||||
}
|
||||
const model = useMemo(() => {
|
||||
const clone = obj.clone(true);
|
||||
// Center + reorient + scale. OBJ length is along Z; rotate so length lies along X.
|
||||
const box = new THREE.Box3().setFromObject(clone);
|
||||
const size = new THREE.Vector3();
|
||||
box.getSize(size);
|
||||
const center = new THREE.Vector3();
|
||||
box.getCenter(center);
|
||||
const maxDim = Math.max(size.x, size.y, size.z);
|
||||
const scale = 3.2 / maxDim;
|
||||
|
||||
clone.position.sub(center.multiplyScalar(scale));
|
||||
clone.scale.setScalar(scale);
|
||||
// Lay the scooter along X and stand it upright (Y up).
|
||||
clone.rotation.y = -Math.PI / 2;
|
||||
clone.rotation.z = 0;
|
||||
|
||||
// Apply a cohesive, premium material across all meshes.
|
||||
const bodyMat = new THREE.MeshStandardMaterial({
|
||||
color: ACCENT,
|
||||
roughness: 0.2,
|
||||
metalness: 0.7,
|
||||
});
|
||||
const accentMat = new THREE.MeshStandardMaterial({
|
||||
color: '#e2e8f0',
|
||||
roughness: 0.1,
|
||||
metalness: 0.9,
|
||||
});
|
||||
const tireMat = new THREE.MeshStandardMaterial({
|
||||
color: '#0f172a',
|
||||
roughness: 0.8,
|
||||
metalness: 0.15,
|
||||
});
|
||||
|
||||
clone.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
child.castShadow = true;
|
||||
child.receiveShadow = true;
|
||||
// Heuristic: pick material by geometry size.
|
||||
const b = new THREE.Box3().setFromObject(child);
|
||||
const s = new THREE.Vector3();
|
||||
b.getSize(s);
|
||||
const maxLocal = Math.max(s.x, s.y, s.z);
|
||||
if (maxLocal < 0.35) child.material = accentMat;
|
||||
else if (s.y < 0.25 && maxLocal > 0.5) child.material = tireMat;
|
||||
else child.material = bodyMat;
|
||||
}
|
||||
});
|
||||
return clone;
|
||||
}, [obj]);
|
||||
|
||||
useFrame((state) => {
|
||||
if (!group.current) return;
|
||||
const t = state.clock.elapsedTime;
|
||||
// Continuous turntable so all sides are appreciated.
|
||||
group.current.rotation.y = t * 0.4;
|
||||
group.current.position.y = Math.sin(t * 1.5) * 0.08;
|
||||
});
|
||||
|
||||
return (
|
||||
<Float speed={2} rotationIntensity={0.5} floatIntensity={1.5}>
|
||||
<group ref={group} position={[2, 0, -2]} scale={1.2} rotation={[0.2, -0.5, 0]}>
|
||||
{/* Main Deck */}
|
||||
<Box args={[4, 0.3, 1]} position={[0, 0, 0]}>
|
||||
<meshStandardMaterial color="#334155" roughness={0.4} metalness={0.6} />
|
||||
</Box>
|
||||
|
||||
{/* Front Wheel */}
|
||||
<Cylinder args={[0.8, 0.8, 0.4, 32]} rotation={[Math.PI / 2, 0, 0]} position={[2, 0, 0]}>
|
||||
<meshStandardMaterial color="#f97316" roughness={0.2} metalness={0.8} />
|
||||
</Cylinder>
|
||||
|
||||
{/* Rear Wheel */}
|
||||
<Cylinder args={[0.8, 0.8, 0.4, 32]} rotation={[Math.PI / 2, 0, 0]} position={[-2, 0, 0]}>
|
||||
<meshStandardMaterial color="#f97316" roughness={0.2} metalness={0.8} />
|
||||
</Cylinder>
|
||||
|
||||
{/* Steering Column */}
|
||||
<Cylinder args={[0.1, 0.1, 3, 16]} position={[1.5, 1.5, 0]} rotation={[0, 0, -0.2]}>
|
||||
<meshStandardMaterial color="#cbd5e1" roughness={0.3} metalness={0.9} />
|
||||
</Cylinder>
|
||||
|
||||
{/* Handlebars */}
|
||||
<Box args={[0.2, 1.5, 0.2]} position={[1.2, 3, 0]} rotation={[Math.PI / 2, 0, 0]}>
|
||||
<meshStandardMaterial color="#0f172a" roughness={0.8} />
|
||||
</Box>
|
||||
|
||||
{/* Accent / Battery block */}
|
||||
<Box args={[1.5, 0.6, 1.1]} position={[-0.5, 0.4, 0]}>
|
||||
<meshStandardMaterial color="#00bc84" emissive="#00bc84" emissiveIntensity={0.5} roughness={0.2} />
|
||||
</Box>
|
||||
<Float speed={1.2} rotationIntensity={0.1} floatIntensity={0.3}>
|
||||
<group ref={group} rotation={[0, -0.5, 0]} position={[0, -0.1, 0]}>
|
||||
<primitive object={model} />
|
||||
</group>
|
||||
</Float>
|
||||
);
|
||||
}
|
||||
|
||||
export default function HeroBackground3D() {
|
||||
function Crystal({ position, color, scale = 1 }: { position: [number, number, number]; color: string; scale?: number }) {
|
||||
const ref = useRef<THREE.Mesh>(null);
|
||||
useFrame((state, delta) => {
|
||||
if (ref.current) {
|
||||
ref.current.rotation.y += delta * 0.5;
|
||||
ref.current.position.y = position[1] + Math.sin(state.clock.elapsedTime + position[0]) * 0.3;
|
||||
}
|
||||
});
|
||||
return (
|
||||
<div className="absolute inset-0 pointer-events-none z-0 overflow-hidden">
|
||||
<Canvas camera={{ position: [0, 0, 8], fov: 45 }}>
|
||||
<ambientLight intensity={0.5} />
|
||||
<directionalLight position={[10, 10, 5]} intensity={1} />
|
||||
<directionalLight position={[-10, -10, -5]} intensity={0.5} color="#00bc84" />
|
||||
<StylizedScooter />
|
||||
</Canvas>
|
||||
</div>
|
||||
<Float speed={3} rotationIntensity={1} floatIntensity={2}>
|
||||
<mesh ref={ref} position={position} scale={scale}>
|
||||
<octahedronGeometry args={[0.4, 0]} />
|
||||
<meshStandardMaterial color={color} emissive={color} emissiveIntensity={0.3} roughness={0.1} metalness={0.3} flatShading />
|
||||
</mesh>
|
||||
</Float>
|
||||
);
|
||||
}
|
||||
|
||||
function Floor() {
|
||||
return (
|
||||
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, -1.1, 0]} receiveShadow>
|
||||
<circleGeometry args={[9, 64]} />
|
||||
<meshStandardMaterial color="#0f172a" roughness={0.6} metalness={0.2} transparent opacity={0.4} />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
|
||||
function Scene() {
|
||||
return (
|
||||
<>
|
||||
<ambientLight intensity={0.4} />
|
||||
<directionalLight position={[6, 8, 5]} intensity={1.4} color="#f8fafc" castShadow />
|
||||
<directionalLight position={[-8, -4, -6]} intensity={0.6} color={ACCENT_BRIGHT} />
|
||||
<pointLight position={[0, 3, 4]} intensity={0.8} color={ACCENT_BRIGHT} />
|
||||
<spotLight position={[0, 10, 0]} angle={0.5} intensity={1.2} color="#ffffff" penumbra={1} />
|
||||
|
||||
<Suspense fallback={<Loader />}>
|
||||
<AeroxModel />
|
||||
</Suspense>
|
||||
|
||||
<Crystal position={[-3.8, 1.6, -1]} color={ACCENT_BRIGHT} scale={1.1} />
|
||||
<Crystal position={[3.8, 1.2, -0.5]} color={ACCENT} scale={0.8} />
|
||||
<Crystal position={[3.4, -0.8, 0.6]} color="#34d399" scale={0.6} />
|
||||
<Crystal position={[-4.0, -0.4, 0.8]} color="#34d399" scale={0.7} />
|
||||
|
||||
<Sparkles count={90} scale={[11, 7, 7]} size={3} speed={0.4} color={ACCENT_BRIGHT} opacity={0.7} />
|
||||
|
||||
<ContactShadows position={[0, -1.06, 0]} opacity={0.55} scale={14} blur={2.5} far={4} color={ACCENT} />
|
||||
<Floor />
|
||||
|
||||
<Environment resolution={256}>
|
||||
<Lightformer intensity={2} position={[0, 5, 5]} scale={[10, 5, 1]} color="#ffffff" />
|
||||
<Lightformer intensity={1.2} position={[-5, 2, 3]} scale={[8, 6, 1]} color={ACCENT_BRIGHT} />
|
||||
<Lightformer intensity={0.8} position={[5, 1, 3]} scale={[8, 6, 1]} color="#0ea5e9" />
|
||||
<Lightformer intensity={0.6} position={[0, -5, 2]} scale={[10, 5, 1]} color="#1e293b" />
|
||||
</Environment>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Rig({ children }: { children: React.ReactNode }) {
|
||||
useFrame((state) => {
|
||||
const { camera, pointer } = state;
|
||||
camera.position.x += (pointer.x * 1.4 - camera.position.x) * 0.04;
|
||||
camera.position.y += (-pointer.y * 0.7 + 0.4 - camera.position.y) * 0.04;
|
||||
camera.lookAt(0, 0, 0);
|
||||
});
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
export default function HeroBackground3D() {
|
||||
return (
|
||||
<div className="absolute inset-0 z-0 overflow-hidden">
|
||||
<Canvas
|
||||
shadows
|
||||
dpr={[1, 2]}
|
||||
camera={{ position: [0, 0.4, 8], fov: 42 }}
|
||||
gl={{ antialias: true, alpha: true, powerPreference: 'high-performance' }}
|
||||
onCreated={({ gl }) => gl.setClearColor(0x000000, 0)}
|
||||
>
|
||||
<Rig>
|
||||
<Scene />
|
||||
</Rig>
|
||||
</Canvas>
|
||||
<div className="pointer-events-none absolute inset-0 bg-gradient-to-r from-slate-900/70 via-slate-900/20 to-transparent lg:from-slate-900/80 lg:via-slate-900/30 lg:to-transparent" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
52
src/components/ui/3d/Logo3D.tsx
Normal file
52
src/components/ui/3d/Logo3D.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
'use client';
|
||||
|
||||
import { Canvas, useFrame } from '@react-three/fiber';
|
||||
import { Float, Box, Sphere } from '@react-three/drei';
|
||||
import { useRef } from 'react';
|
||||
import * as THREE from 'three';
|
||||
|
||||
function RotatingLogo({ isScrolled }: { isScrolled: boolean }) {
|
||||
const group = useRef<THREE.Group>(null);
|
||||
|
||||
useFrame((state, delta) => {
|
||||
if (group.current) {
|
||||
group.current.rotation.y += delta;
|
||||
group.current.rotation.x = Math.sin(state.clock.elapsedTime * 2) * 0.1;
|
||||
}
|
||||
});
|
||||
|
||||
const color = isScrolled ? '#00bc84' : '#10b981';
|
||||
|
||||
return (
|
||||
<Float speed={3} rotationIntensity={0.5} floatIntensity={2}>
|
||||
<group ref={group} scale={1.2}>
|
||||
{/* Abstract "J" shape for Jaiben */}
|
||||
<Box args={[0.25, 1.2, 0.25]} position={[0.2, 0, 0]}>
|
||||
<meshStandardMaterial color={color} roughness={0.1} metalness={0.8} />
|
||||
</Box>
|
||||
<Box args={[0.8, 0.25, 0.25]} position={[-0.075, -0.475, 0]}>
|
||||
<meshStandardMaterial color={color} roughness={0.1} metalness={0.8} />
|
||||
</Box>
|
||||
<Sphere args={[0.2, 32, 32]} position={[-0.3, 0.5, 0]}>
|
||||
<meshStandardMaterial color="#f97316" roughness={0.2} metalness={0.5} emissive="#f97316" emissiveIntensity={0.2} />
|
||||
</Sphere>
|
||||
</group>
|
||||
</Float>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Logo3D({ scrolled = false, className = "w-12 h-12" }: { scrolled?: boolean, className?: string }) {
|
||||
return (
|
||||
<div className={`${className} pointer-events-none relative z-10 flex-shrink-0`}>
|
||||
<Canvas
|
||||
camera={{ position: [0, 0, 3.5], fov: 50 }}
|
||||
gl={{ alpha: true, antialias: true }}
|
||||
>
|
||||
<ambientLight intensity={0.6} />
|
||||
<directionalLight position={[2, 5, 2]} intensity={1.5} />
|
||||
<directionalLight position={[-2, -2, 2]} intensity={0.5} color="#00bc84" />
|
||||
<RotatingLogo isScrolled={scrolled} />
|
||||
</Canvas>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user