156 lines
8.2 KiB
TypeScript
156 lines
8.2 KiB
TypeScript
// app/projects/design/page.tsx
|
|
|
|
'use client';
|
|
|
|
import { Center, Environment, Loader, OrbitControls, useGLTF } from '@react-three/drei';
|
|
import { Canvas, useThree } from '@react-three/fiber';
|
|
import { Suspense, useEffect, useState, useTransition } from 'react';
|
|
import { TbCube3dSphere } from 'react-icons/tb';
|
|
import * as THREE from 'three';
|
|
|
|
const models = [
|
|
{ name: 'Wallet', src: 'Wallet' },
|
|
{ name: 'Car', src: 'Car' },
|
|
{ name: 'Magnet holder', src: 'Magnet holder' },
|
|
{ name: 'PC', src: 'PC' },
|
|
];
|
|
|
|
function Model({ url, onReady }: { url: string; onReady: () => void }) {
|
|
const { scene } = useGLTF(url);
|
|
|
|
useEffect(() => {
|
|
if (scene) {
|
|
const timer = setTimeout(onReady, 100);
|
|
return () => clearTimeout(timer);
|
|
}
|
|
}, [scene, onReady]);
|
|
|
|
return <primitive object={scene} />;
|
|
}
|
|
|
|
function DynamicCamera({ radius }: { radius: number }) {
|
|
const { camera } = useThree();
|
|
useEffect(() => {
|
|
camera.position.set(0, radius * 0.5, radius * 2.5);
|
|
camera.lookAt(0, 0, 0);
|
|
camera.updateProjectionMatrix();
|
|
}, [radius, camera]);
|
|
return null;
|
|
}
|
|
|
|
export default function DesignPage() {
|
|
const [activeModel, setActiveModel] = useState(models[0].src);
|
|
const [modelRadius, setModelRadius] = useState(1);
|
|
const [isPending, startTransition] = useTransition();
|
|
const [isSceneReady, setIsSceneReady] = useState(false);
|
|
const [isCentering, setIsCentering] = useState(true);
|
|
|
|
const handleModelChange = (newModel: string) => {
|
|
if (newModel === activeModel) return;
|
|
|
|
setIsCentering(true);
|
|
setIsSceneReady(false);
|
|
|
|
startTransition(() => {
|
|
setActiveModel(newModel);
|
|
});
|
|
};
|
|
|
|
const showOverlay = isPending || !isSceneReady;
|
|
|
|
return (
|
|
<main className="flex flex-col flex-1 relative bg-[#0d1117] overflow-hidden">
|
|
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-200 h-200 bg-[#84c0a0]/10 blur-[150px] rounded-full pointer-events-none z-0"></div>
|
|
<div className="h-25 shrink-0"></div>
|
|
|
|
<div className="relative z-10 w-full max-w-350 mx-auto px-4 pb-20 flex flex-col gap-10">
|
|
<div className="pt-5 flex justify-center items-center flex-col text-center">
|
|
<TbCube3dSphere className="text-white text-[80px] drop-shadow-[0_0_15px_rgba(255,255,255,0.2)]" />
|
|
<h2 className="text-white text-[32px] md:text-[45px] uppercase mt-3.75 font-bold tracking-wider">CAD Library</h2>
|
|
</div>
|
|
|
|
<section className="w-full flex justify-center">
|
|
<div className="w-full relative bg-white/5 backdrop-blur-xl rounded-4xl border border-white/10 shadow-[0_8px_32px_rgba(0,0,0,0.5)] p-4 md:p-8 flex flex-col">
|
|
<div className="w-full h-[50vh] md:h-[65vh] rounded-[20px] overflow-hidden relative bg-linear-to-br from-[#1a1a1a] to-[#0f0f0f] shadow-inner border border-white/5 cursor-grab active:cursor-grabbing">
|
|
|
|
{showOverlay && (
|
|
<div className="absolute inset-0 z-20 flex flex-col items-center justify-center bg-[#0d1117]/80 backdrop-blur-md transition-all duration-500">
|
|
<div className="w-12 h-12 border-4 border-[#84c0a0]/20 border-t-[#84c0a0] rounded-full animate-spin mb-4"></div>
|
|
<p className="text-[#84c0a0] font-mono text-xs uppercase tracking-[0.2em] font-bold">
|
|
{isPending ? "Fetching Data..." : "Compiling Materials..."}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
<Canvas shadows={{ type: THREE.PCFShadowMap }} dpr={[1, 2]} camera={{ near: 0.001, far: 1000, fov: 50 }}>
|
|
<ambientLight intensity={0.5} />
|
|
<directionalLight position={[10, 10, 10]} intensity={1} castShadow />
|
|
<Environment preset="city" />
|
|
<DynamicCamera radius={modelRadius} />
|
|
|
|
<Suspense fallback={null}>
|
|
<Center
|
|
key={activeModel}
|
|
onCentered={({ boundingSphere }) => {
|
|
setModelRadius(boundingSphere.radius);
|
|
requestAnimationFrame(() => setIsCentering(false));
|
|
}}
|
|
>
|
|
<group visible={!isCentering && isSceneReady}>
|
|
<Model
|
|
url={`/models/${activeModel}.glb`}
|
|
onReady={() => setIsSceneReady(true)}
|
|
/>
|
|
</group>
|
|
</Center>
|
|
</Suspense>
|
|
|
|
<OrbitControls
|
|
makeDefault
|
|
autoRotate
|
|
autoRotateSpeed={0.5}
|
|
enablePan={true}
|
|
enableZoom={true}
|
|
minDistance={modelRadius * 0.5}
|
|
maxDistance={modelRadius * 6}
|
|
/>
|
|
</Canvas>
|
|
|
|
<Loader
|
|
containerStyles={{ background: 'transparent' }}
|
|
innerStyles={{ display: 'none' }}
|
|
/>
|
|
|
|
<a href={`/models/${activeModel}.usdz`} rel="ar" className="absolute top-4 right-4 bg-black/50 backdrop-blur-md text-white text-sm px-4 py-2 rounded-full border border-white/10 hover:bg-white/20 transition-colors md:hidden z-10 flex items-center gap-2">
|
|
<img src={`/models/${activeModel}.png`} alt="AR Preview" className="w-5 h-5 rounded-full object-cover" />
|
|
View in AR
|
|
</a>
|
|
</div>
|
|
|
|
<div className="w-full mt-8 flex justify-center">
|
|
<div className="flex flex-wrap justify-center gap-4 md:gap-8 w-full px-2">
|
|
{models.map((model) => (
|
|
<button
|
|
key={model.name}
|
|
className={`shrink-0 w-20 h-20 md:w-25 md:h-25 bg-contain bg-no-repeat bg-center rounded-2xl transition-all duration-300 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#84c0a0] relative group ${activeModel === model.src
|
|
? 'bg-[#2a2a2a] border-2 border-[#84c0a0] scale-110 shadow-[0_0_20px_rgba(132,192,160,0.3)]'
|
|
: 'bg-[#1a1a1a] border border-white/10 hover:bg-[#252525] hover:-translate-y-1'
|
|
}`}
|
|
style={{ backgroundImage: `url('/models/${model.src}.png')` }}
|
|
onClick={() => handleModelChange(model.src)}
|
|
onMouseEnter={() => useGLTF.preload(`/models/${model.src}.glb`)}
|
|
aria-label={`View ${model.name} model`}
|
|
>
|
|
<span className="absolute -bottom-8 left-1/2 -translate-x-1/2 text-xs font-semibold text-white opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap bg-black/80 px-2 py-1 rounded">
|
|
{model.name}
|
|
</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</main>
|
|
);
|
|
} |