'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(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 ( ); } // ================== BOWL ================== function Bowl() { return ( {/* Outer bowl */} {/* Inner bowl wall */} {/* Bowl bottom inside */} ); } // ================== GRAVY ================== function Gravy() { return ( {/* Main thick gravy */} {/* Creamy top layer */} {/* Glossy highlights layer */} ); } // ================== 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) => ( {/* Main chicken body */} {/* Extra volume */} ))} ); } // ================== 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 ( ); })} {/* Small spice bits */} {Array.from({ length: 26 }).map((_, i) => ( ))} ); } // ================== MAIN MODEL ================== function ButterChickenModel() { return ( {/* Rising Smoke */} ); } // ================== MAIN EXPORT ================== export default function ButterChicken3D() { return (
{/* Warm restaurant lighting */}
DRAG TO ROTATE • SCROLL TO ZOOM
); }