Files
shahikitchen/components/ButterChicken3D.tsx
Zeeshan Khan 56fe68eb48 Initial commit: Shahi Kitchen premium website
- 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
2026-06-01 15:14:19 +02:00

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