websitepratik / components /Loader /EmberLoader.tsx
Antaram's picture
Upload 29 files
efb726d verified
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: 4060%"
}}
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;