Spaces:
Sleeping
Sleeping
| import React, { useEffect, useState, useMemo, useRef } from 'react'; | |
| import { motion } from 'framer-motion'; | |
| import { useAudio } from '../../context/AudioContext'; | |
| interface EmberLoaderProps { | |
| onLoadingComplete: () => void; | |
| } | |
| const EmberLoader: React.FC<EmberLoaderProps> = ({ onLoadingComplete }) => { | |
| const [progress, setProgress] = useState(0); | |
| const { playTickSound } = useAudio(); | |
| // Ref to track the last played tick integer to prevent spamming within the same integer frame | |
| const lastTickRef = useRef(0); | |
| // Configuration | |
| const DURATION = 8000; // 8 seconds total loading time | |
| // Background Image: Using the dark, textured chilli image for the "Base image" anchor | |
| // Premium quality, dark, textured. | |
| const BG_IMAGE = "https://i.imgur.com/0D9HPtG.gif"; | |
| useEffect(() => { | |
| const startTime = Date.now(); | |
| const updateProgress = () => { | |
| const now = Date.now(); | |
| const elapsed = now - startTime; | |
| if (elapsed < DURATION) { | |
| // Organic easing: slow start, fast middle, slow end (easeInOutCubic) | |
| const t = elapsed / DURATION; | |
| const ease = t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; | |
| const currentProgress = Math.min(100, Math.round(ease * 100)); | |
| // Audio Logic: Play tick if integer value changed | |
| if (currentProgress > lastTickRef.current) { | |
| playTickSound(); | |
| lastTickRef.current = currentProgress; | |
| } | |
| setProgress(currentProgress); | |
| requestAnimationFrame(updateProgress); | |
| } else { | |
| setProgress(100); | |
| setTimeout(onLoadingComplete, 800); | |
| } | |
| }; | |
| const animationHandle = requestAnimationFrame(updateProgress); | |
| return () => cancelAnimationFrame(animationHandle); | |
| }, [onLoadingComplete, playTickSound]); | |
| // --- Micro Ember Drift (Foreground) --- | |
| // "Only the tiny chilli particles move. Direction: slow upward + slight random sideways. Speed: 1–2px per second." | |
| const particles = useMemo(() => Array.from({ length: 25 }).map((_, i) => ({ | |
| id: i, | |
| left: Math.random() * 100, // Random horizontal position | |
| bottom: Math.random() * 80, // Start mostly lower/middle | |
| size: Math.random() * 2 + 1, // 1-3px (Tiny) | |
| duration: Math.random() * 4 + 4, // 4-8s duration (Slow) | |
| xOffset: (Math.random() - 0.5) * 30, // Slight sideways drift | |
| delay: Math.random() * 2, | |
| })), []); | |
| return ( | |
| <motion.div | |
| className="fixed inset-0 z-[9999] bg-[#0f0202] overflow-hidden cursor-none flex items-center justify-center" | |
| initial={{ opacity: 1 }} | |
| exit={{ | |
| opacity: 0, | |
| transition: { duration: 1.5, ease: "easeInOut" } // "Hero text fades in after" handled by App.tsx delay usually, but long exit here helps | |
| }} | |
| > | |
| {/* 1. Base Image (Static Anchor) - Using IMG tag for better scaling consistency */} | |
| {/* 4. Glow Breathing (Global) - "Overall image brightness pulses gently." */} | |
| <motion.div | |
| className="absolute inset-0 z-0 w-full h-full" | |
| initial={{ scale: 1.02, filter: "brightness(0.5) contrast(1.1)" }} | |
| animate={{ | |
| filter: [ | |
| "brightness(0.5) contrast(1.1)", | |
| "brightness(0.6) contrast(1.15)", | |
| "brightness(0.5) contrast(1.1)" | |
| ], | |
| // 5. Focus Drift - subtle scale breathing | |
| scale: [1.02, 1.04, 1.02] | |
| }} | |
| transition={{ | |
| duration: 3.2, // ~3.2s loop | |
| repeat: Infinity, | |
| ease: "easeInOut" | |
| }} | |
| > | |
| <img | |
| src={BG_IMAGE} | |
| alt="Loading Texture" | |
| className="w-full h-full object-cover" | |
| /> | |
| {/* Vignette to focus attention */} | |
| <div className="absolute inset-0 bg-radial-gradient from-transparent via-black/40 to-black/80" /> | |
| </motion.div> | |
| {/* 3. Heat Shimmer (Critical) */} | |
| {/* "Vertical distortion only. Strength: 2–4% max." */} | |
| <div | |
| className="absolute inset-0 z-10 opacity-30 pointer-events-none mix-blend-overlay" | |
| style={{ filter: 'url(#heat-haze)' }} | |
| > | |
| {/* A gradient overlay that gets distorted by the filter */} | |
| <div className="absolute inset-0 bg-gradient-to-t from-transparent via-white/5 to-transparent" /> | |
| </div> | |
| {/* 2. Micro Ember Drift (Foreground) */} | |
| <div className="absolute inset-0 z-20 pointer-events-none"> | |
| {particles.map((p) => ( | |
| <motion.div | |
| key={p.id} | |
| className="absolute rounded-full bg-brand-red/80 blur-[1px]" | |
| style={{ | |
| left: `${p.left}%`, | |
| bottom: `${p.bottom}%`, | |
| width: p.size, | |
| height: p.size, | |
| }} | |
| animate={{ | |
| y: [0, -40], // Slow upward movement (~20-40px over 4-8s is slow) | |
| x: [0, p.xOffset], // Slight sideways | |
| opacity: [0, 0.6, 0.3, 0], // "Opacity: 40–60%" | |
| }} | |
| transition={{ | |
| duration: p.duration, | |
| repeat: Infinity, | |
| delay: p.delay, | |
| ease: "linear" | |
| }} | |
| /> | |
| ))} | |
| </div> | |
| {/* Red Atmospheric Glow (Bottom) */} | |
| <div className="absolute bottom-0 left-0 right-0 h-1/2 bg-gradient-to-t from-[#2a0505] to-transparent opacity-80 z-10" /> | |
| {/* Bottom Logic: Doto Fonts / Percentage */} | |
| {/* "No loading text" interpreted as removing labels, keeping only the metric */} | |
| <div className="absolute bottom-12 right-12 z-30 flex items-end"> | |
| <motion.div | |
| initial={{ opacity: 0 }} | |
| animate={{ opacity: 1 }} | |
| transition={{ duration: 1 }} | |
| className="flex items-baseline gap-1" | |
| > | |
| <span className="font-doto text-2xl md:text-3xl text-white/90 font-bold tabular-nums tracking-widest"> | |
| {progress.toString().padStart(3, '0')} | |
| </span> | |
| <span className="font-doto text-sm text-brand-gold/70 font-bold"> | |
| % | |
| </span> | |
| </motion.div> | |
| </div> | |
| {/* Brand Name - Centered & Adjusted Size (Responsive) */} | |
| {/* Must be one line, sync with background */} | |
| <div className="absolute inset-0 z-40 flex flex-col items-center justify-center px-4 pointer-events-none"> | |
| <motion.div | |
| initial={{ opacity: 0, scale: 0.95, filter: "blur(5px)" }} | |
| animate={{ opacity: 1, scale: 1, filter: "blur(0px)" }} | |
| transition={{ duration: 1.2, ease: "easeOut", delay: 0.5 }} | |
| className="text-center w-full" | |
| > | |
| <h1 className="font-serif text-3xl md:text-5xl lg:text-7xl font-bold text-white tracking-[0.1em] uppercase drop-shadow-[0_4px_10px_rgba(0,0,0,0.5)] whitespace-nowrap overflow-hidden text-ellipsis"> | |
| C.S. PATTANSHETTI | |
| </h1> | |
| {/* Elegant divider */} | |
| <motion.div | |
| initial={{ width: 0, opacity: 0 }} | |
| animate={{ width: "120px", opacity: 0.8 }} | |
| transition={{ duration: 1.5, delay: 1, ease: [0.22, 1, 0.36, 1] }} | |
| className="h-[1px] bg-gradient-to-r from-transparent via-brand-gold to-transparent mx-auto mt-4 md:mt-6" | |
| /> | |
| </motion.div> | |
| </div> | |
| </motion.div> | |
| ); | |
| }; | |
| export default EmberLoader; |