feat: add 3D scooter model and landing page layout enhancements

This commit is contained in:
sazzadulalambd
2026-06-22 16:51:48 +06:00
parent 87d3421606
commit eb38a01976
5 changed files with 1778497 additions and 74 deletions

View File

@@ -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>
);
}

View 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>
);
}