56fe68eb48
- Royal cream + gold theme - Playful animated hero with chef mascot - Advanced menu with sidebar + video hover - Multilingual support (EN, SV, HI, UR) - Cart system with WhatsApp ordering - Real restaurant photos integration - Responsive design with proper navbar
321 lines
9.1 KiB
TypeScript
321 lines
9.1 KiB
TypeScript
'use client';
|
|
|
|
import { Canvas, useFrame } from '@react-three/fiber';
|
|
import { OrbitControls, Environment } from '@react-three/drei';
|
|
import { Suspense, useMemo, useRef } from 'react';
|
|
import * as THREE from 'three';
|
|
|
|
// ================== SMOKE PARTICLES ==================
|
|
function SmokeParticles({ count = 120 }: { count?: number }) {
|
|
const pointsRef = useRef<THREE.Points>(null!);
|
|
|
|
const particles = useMemo(() => {
|
|
const positions = new Float32Array(count * 3);
|
|
const velocities = new Float32Array(count * 3);
|
|
const ages = new Float32Array(count);
|
|
const sizes = new Float32Array(count);
|
|
|
|
for (let i = 0; i < count; i++) {
|
|
const i3 = i * 3;
|
|
|
|
// Start around the top of the gravy
|
|
positions[i3 + 0] = (Math.random() - 0.5) * 2.2;
|
|
positions[i3 + 1] = 0.6 + Math.random() * 0.3;
|
|
positions[i3 + 2] = (Math.random() - 0.5) * 2.0;
|
|
|
|
velocities[i3 + 0] = (Math.random() - 0.5) * 0.008;
|
|
velocities[i3 + 1] = 0.012 + Math.random() * 0.018;
|
|
velocities[i3 + 2] = (Math.random() - 0.5) * 0.008;
|
|
|
|
ages[i] = Math.random() * 3.5;
|
|
sizes[i] = 0.12 + Math.random() * 0.18;
|
|
}
|
|
|
|
return { positions, velocities, ages, sizes };
|
|
}, [count]);
|
|
|
|
useFrame((state, delta) => {
|
|
const points = pointsRef.current;
|
|
if (!points) return;
|
|
|
|
const pos = points.geometry.attributes.position as THREE.BufferAttribute;
|
|
const posArray = pos.array as Float32Array;
|
|
|
|
for (let i = 0; i < count; i++) {
|
|
const i3 = i * 3;
|
|
|
|
// Age and reset
|
|
particles.ages[i] += delta * 0.9;
|
|
|
|
if (particles.ages[i] > 3.8) {
|
|
// Respawn at the top of the gravy
|
|
particles.ages[i] = 0;
|
|
posArray[i3 + 0] = (Math.random() - 0.5) * 2.1;
|
|
posArray[i3 + 1] = 0.55 + Math.random() * 0.15;
|
|
posArray[i3 + 2] = (Math.random() - 0.5) * 1.9;
|
|
particles.velocities[i3 + 0] = (Math.random() - 0.5) * 0.009;
|
|
particles.velocities[i3 + 1] = 0.014 + Math.random() * 0.02;
|
|
} else {
|
|
// Rise with some turbulence
|
|
posArray[i3 + 0] += particles.velocities[i3 + 0] + Math.sin(state.clock.elapsedTime * 1.5 + i) * 0.002;
|
|
posArray[i3 + 1] += particles.velocities[i3 + 1];
|
|
posArray[i3 + 2] += particles.velocities[i3 + 2] + Math.cos(state.clock.elapsedTime * 1.2 + i) * 0.002;
|
|
|
|
// Slow down as it rises
|
|
particles.velocities[i3 + 1] *= 0.992;
|
|
}
|
|
}
|
|
|
|
pos.needsUpdate = true;
|
|
});
|
|
|
|
const geometry = useMemo(() => {
|
|
const geo = new THREE.BufferGeometry();
|
|
geo.setAttribute('position', new THREE.BufferAttribute(particles.positions, 3));
|
|
return geo;
|
|
}, [particles.positions]);
|
|
|
|
return (
|
|
<points ref={pointsRef} geometry={geometry}>
|
|
<pointsMaterial
|
|
size={0.22}
|
|
color="#f4e9d8"
|
|
transparent
|
|
opacity={0.32}
|
|
depthWrite={false}
|
|
sizeAttenuation={true}
|
|
/>
|
|
</points>
|
|
);
|
|
}
|
|
|
|
// ================== BOWL ==================
|
|
function Bowl() {
|
|
return (
|
|
<group>
|
|
{/* Outer bowl */}
|
|
<mesh position={[0, -0.25, 0]} castShadow receiveShadow>
|
|
<cylinderGeometry args={[2.35, 1.95, 1.95, 72, 1, true]} />
|
|
<meshPhongMaterial
|
|
color="#2f2723"
|
|
shininess={45}
|
|
specular="#3a2f2a"
|
|
side={THREE.DoubleSide}
|
|
/>
|
|
</mesh>
|
|
|
|
{/* Inner bowl wall */}
|
|
<mesh position={[0, -0.25, 0]} castShadow receiveShadow>
|
|
<cylinderGeometry args={[2.1, 1.72, 1.85, 72, 1, true]} />
|
|
<meshPhongMaterial
|
|
color="#1f1a17"
|
|
shininess={25}
|
|
side={THREE.DoubleSide}
|
|
/>
|
|
</mesh>
|
|
|
|
{/* Bowl bottom inside */}
|
|
<mesh position={[0, -1.15, 0]} receiveShadow>
|
|
<cylinderGeometry args={[1.72, 1.72, 0.12, 72]} />
|
|
<meshPhongMaterial color="#1f1a17" />
|
|
</mesh>
|
|
</group>
|
|
);
|
|
}
|
|
|
|
// ================== GRAVY ==================
|
|
function Gravy() {
|
|
return (
|
|
<group>
|
|
{/* Main thick gravy */}
|
|
<mesh position={[0, 0.05, 0]} receiveShadow>
|
|
<cylinderGeometry args={[2.0, 1.65, 1.35, 72]} />
|
|
<meshPhongMaterial
|
|
color="#d46f2e"
|
|
shininess={95}
|
|
specular="#ffe8c4"
|
|
/>
|
|
</mesh>
|
|
|
|
{/* Creamy top layer */}
|
|
<mesh position={[0, 0.55, 0]}>
|
|
<cylinderGeometry args={[1.92, 1.62, 0.42, 72]} />
|
|
<meshPhongMaterial
|
|
color="#f0a45f"
|
|
shininess={120}
|
|
specular="#fff4d9"
|
|
/>
|
|
</mesh>
|
|
|
|
{/* Glossy highlights layer */}
|
|
<mesh position={[0, 0.68, 0]}>
|
|
<cylinderGeometry args={[1.78, 1.55, 0.18, 72]} />
|
|
<meshPhongMaterial
|
|
color="#f8c48a"
|
|
shininess={140}
|
|
specular="#ffffff"
|
|
transparent
|
|
opacity={0.65}
|
|
/>
|
|
</mesh>
|
|
</group>
|
|
);
|
|
}
|
|
|
|
// ================== CHICKEN PIECES ==================
|
|
function ChickenPieces() {
|
|
const pieces = [
|
|
{ pos: [0.6, 0.45, 0.1], rot: [0.6, 1.2, 0.3], scale: 1.0 },
|
|
{ pos: [-0.75, 0.38, 0.55], rot: [-0.4, -0.9, 0.5], scale: 0.95 },
|
|
{ pos: [0.15, 0.52, -0.85], rot: [0.9, 0.4, -0.6], scale: 1.05 },
|
|
{ pos: [-0.55, 0.42, -0.4], rot: [-0.7, 1.6, 0.2], scale: 0.9 },
|
|
{ pos: [0.9, 0.35, -0.55], rot: [0.3, -1.1, -0.4], scale: 0.98 },
|
|
{ pos: [-0.2, 0.48, 0.75], rot: [-0.5, 0.7, 0.8], scale: 0.92 },
|
|
];
|
|
|
|
return (
|
|
<>
|
|
{pieces.map((p, i) => (
|
|
<group key={i} position={p.pos as [number, number, number]} rotation={p.rot as [number, number, number]} scale={p.scale}>
|
|
{/* Main chicken body */}
|
|
<mesh castShadow>
|
|
<capsuleGeometry args={[0.42, 0.65, 8]} />
|
|
<meshPhongMaterial
|
|
color="#b84f25"
|
|
shininess={55}
|
|
specular="#3a1f14"
|
|
/>
|
|
</mesh>
|
|
|
|
{/* Extra volume */}
|
|
<mesh position={[0.12, 0.08, -0.1]} castShadow>
|
|
<sphereGeometry args={[0.38]} />
|
|
<meshPhongMaterial color="#a64520" shininess={40} />
|
|
</mesh>
|
|
</group>
|
|
))}
|
|
</>
|
|
);
|
|
}
|
|
|
|
// ================== HERBS & SPICES ==================
|
|
function Toppings() {
|
|
return (
|
|
<>
|
|
{/* Green herbs */}
|
|
{Array.from({ length: 18 }).map((_, i) => {
|
|
const angle = i * 0.7 + (i % 3) * 0.3;
|
|
const radius = 0.55 + (i % 4) * 0.22;
|
|
return (
|
|
<mesh
|
|
key={`herb-${i}`}
|
|
position={[
|
|
Math.cos(angle) * radius,
|
|
0.82,
|
|
Math.sin(angle) * radius * 0.9
|
|
]}
|
|
rotation={[Math.random() - 0.5, i, Math.random() - 0.5]}
|
|
>
|
|
<planeGeometry args={[0.22, 0.09]} />
|
|
<meshPhongMaterial
|
|
color="#2d5c3f"
|
|
side={THREE.DoubleSide}
|
|
transparent
|
|
opacity={0.85}
|
|
/>
|
|
</mesh>
|
|
);
|
|
})}
|
|
|
|
{/* Small spice bits */}
|
|
{Array.from({ length: 26 }).map((_, i) => (
|
|
<mesh
|
|
key={`spice-${i}`}
|
|
position={[
|
|
(Math.random() - 0.5) * 2.6,
|
|
0.78 + Math.random() * 0.12,
|
|
(Math.random() - 0.5) * 2.3
|
|
]}
|
|
>
|
|
<sphereGeometry args={[0.035 + Math.random() * 0.025]} />
|
|
<meshPhongMaterial color={i % 4 === 0 ? "#8c3a1f" : "#3a2a1f"} />
|
|
</mesh>
|
|
))}
|
|
</>
|
|
);
|
|
}
|
|
|
|
// ================== MAIN MODEL ==================
|
|
function ButterChickenModel() {
|
|
return (
|
|
<group>
|
|
<Bowl />
|
|
<Gravy />
|
|
<ChickenPieces />
|
|
<Toppings />
|
|
|
|
{/* Rising Smoke */}
|
|
<SmokeParticles count={95} />
|
|
</group>
|
|
);
|
|
}
|
|
|
|
// ================== MAIN EXPORT ==================
|
|
export default function ButterChicken3D() {
|
|
return (
|
|
<div className="w-full h-full min-h-[420px] rounded-2xl overflow-hidden bg-[#F2EDE4] relative">
|
|
<Canvas
|
|
camera={{ position: [0, 3.8, 8.5], fov: 40 }}
|
|
style={{ background: 'transparent' }}
|
|
gl={{
|
|
antialias: true,
|
|
alpha: true,
|
|
preserveDrawingBuffer: true,
|
|
toneMapping: THREE.ACESFilmicToneMapping,
|
|
toneMappingExposure: 1.05,
|
|
}}
|
|
>
|
|
<Suspense fallback={null}>
|
|
{/* Warm restaurant lighting */}
|
|
<ambientLight intensity={0.28} color="#fff4e6" />
|
|
|
|
<directionalLight
|
|
position={[7, 13, 5]}
|
|
intensity={1.85}
|
|
color="#fff0d0"
|
|
castShadow
|
|
/>
|
|
|
|
<directionalLight
|
|
position={[-7, 5, -8]}
|
|
intensity={0.95}
|
|
color="#ffe8c4"
|
|
/>
|
|
|
|
<pointLight position={[-3, 4, 7]} intensity={0.7} color="#fff8e7" />
|
|
|
|
<ButterChickenModel />
|
|
|
|
<Environment preset="apartment" />
|
|
</Suspense>
|
|
|
|
<OrbitControls
|
|
enablePan={false}
|
|
enableZoom={true}
|
|
minDistance={4.5}
|
|
maxDistance={13}
|
|
minPolarAngle={Math.PI * 0.18}
|
|
maxPolarAngle={Math.PI * 0.82}
|
|
enableDamping
|
|
dampingFactor={0.12}
|
|
autoRotate={false}
|
|
/>
|
|
</Canvas>
|
|
|
|
<div className="absolute bottom-3 right-3 text-[10px] text-[#8A8478] tracking-widest pointer-events-none">
|
|
DRAG TO ROTATE • SCROLL TO ZOOM
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|