skills/premium-frontend-design/templates/preloader.tsx

494 lines
13 KiB
TypeScript

/**
* Template: Cinematic Preloader
*
* A reusable preloader component with customizable themes.
* Features: Boot sequence, progress indicator, text scramble, canvas animation
*/
import React, { useState, useEffect, useRef } from 'react';
import { motion } from 'framer-motion';
// =============================================================================
// TYPES
// =============================================================================
interface PreloaderProps {
onComplete: () => void;
theme?: 'cyber' | 'minimal' | 'matrix';
accentColor?: string;
bootMessages?: string[];
duration?: number;
}
interface PreloaderTheme {
background: string;
accent: string;
text: string;
muted: string;
}
// =============================================================================
// THEMES
// =============================================================================
const THEMES: Record<string, PreloaderTheme> = {
cyber: {
background: '#050505',
accent: '#ff4d00',
text: '#ffffff',
muted: '#71717a',
},
minimal: {
background: '#0a0a0a',
accent: '#ffffff',
text: '#ffffff',
muted: '#52525b',
},
matrix: {
background: '#000000',
accent: '#00ff00',
text: '#00ff00',
muted: '#003300',
},
};
// =============================================================================
// DEFAULT BOOT MESSAGES
// =============================================================================
const DEFAULT_BOOT_MESSAGES = [
'INITIALIZING CORE SYSTEMS...',
'LOADING NEURAL NETWORKS...',
'ESTABLISHING SECURE CONNECTION...',
'SYNCING DATA PROTOCOLS...',
'CALIBRATING INTERFACE...',
'SYSTEM READY',
];
// =============================================================================
// HOOKS
// =============================================================================
/**
* Text scramble effect hook
*/
const useTextScramble = (finalText: string, trigger: boolean, speed = 30) => {
const [displayText, setDisplayText] = useState('');
const chars = '!<>-_\\/[]{}—=+*^?#________';
useEffect(() => {
if (!trigger) {
setDisplayText('');
return;
}
let iteration = 0;
const interval = setInterval(() => {
setDisplayText(
finalText
.split('')
.map((char, i) => {
if (char === ' ') return ' ';
if (i < iteration) return char;
return chars[Math.floor(Math.random() * chars.length)];
})
.join('')
);
iteration += 1 / 3;
if (iteration >= finalText.length) {
clearInterval(interval);
setDisplayText(finalText);
}
}, speed);
return () => clearInterval(interval);
}, [finalText, trigger, speed]);
return displayText;
};
// =============================================================================
// CANVAS ANIMATIONS
// =============================================================================
/**
* Data Grid Canvas Animation
*/
const DataGridCanvas: React.FC<{ accent: string }> = ({ accent }) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
const resize = () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
};
resize();
window.addEventListener('resize', resize);
const cellSize = 25;
let scanY = 0;
let animationId: number;
const animate = () => {
ctx.fillStyle = '#050505';
ctx.fillRect(0, 0, canvas.width, canvas.height);
const cols = Math.ceil(canvas.width / cellSize);
const rows = Math.ceil(canvas.height / cellSize);
// Draw active cells near scan line
for (let y = 0; y < rows; y++) {
for (let x = 0; x < cols; x++) {
const distFromScan = Math.abs(y * cellSize - scanY);
if (distFromScan < 80 && Math.random() > 0.75) {
const alpha = 1 - distFromScan / 80;
ctx.fillStyle = accent + Math.floor(alpha * 255).toString(16).padStart(2, '0');
ctx.fillRect(
x * cellSize + 2,
y * cellSize + 2,
cellSize - 4,
cellSize - 4
);
}
}
}
// Draw scan line
ctx.fillStyle = accent;
ctx.fillRect(0, scanY - 1, canvas.width, 2);
ctx.fillStyle = accent + '40';
ctx.fillRect(0, scanY - 20, canvas.width, 40);
scanY += 4;
if (scanY > canvas.height) scanY = 0;
animationId = requestAnimationFrame(animate);
};
animate();
return () => {
cancelAnimationFrame(animationId);
window.removeEventListener('resize', resize);
};
}, [accent]);
return <canvas ref={canvasRef} className="absolute inset-0" />;
};
/**
* Matrix Rain Canvas Animation
*/
const MatrixCanvas: React.FC<{ accent: string }> = ({ accent }) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@#$%^&*';
const fontSize = 14;
const columns = Math.floor(canvas.width / fontSize);
const drops: number[] = Array(columns).fill(1);
let animationId: number;
const animate = () => {
ctx.fillStyle = 'rgba(0, 0, 0, 0.05)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = accent;
ctx.font = `${fontSize}px monospace`;
for (let i = 0; i < drops.length; i++) {
const char = chars[Math.floor(Math.random() * chars.length)];
ctx.fillText(char, i * fontSize, drops[i] * fontSize);
if (drops[i] * fontSize > canvas.height && Math.random() > 0.975) {
drops[i] = 0;
}
drops[i]++;
}
animationId = requestAnimationFrame(animate);
};
animate();
return () => cancelAnimationFrame(animationId);
}, [accent]);
return <canvas ref={canvasRef} className="absolute inset-0" />;
};
// =============================================================================
// PROGRESS INDICATORS
// =============================================================================
/**
* Circular HUD Progress
*/
const CircularProgress: React.FC<{
progress: number;
accent: string;
text: string;
}> = ({ progress, accent, text }) => {
const circumference = 2 * Math.PI * 58;
const strokeDashoffset = circumference - (circumference * progress) / 100;
return (
<div className="relative w-36 h-36">
<svg className="w-full h-full -rotate-90" viewBox="0 0 128 128">
{/* Background circle */}
<circle
cx="64"
cy="64"
r="58"
fill="none"
stroke="rgba(255,255,255,0.1)"
strokeWidth="4"
/>
{/* Progress circle */}
<circle
cx="64"
cy="64"
r="58"
fill="none"
stroke={accent}
strokeWidth="4"
strokeLinecap="round"
strokeDasharray={circumference}
strokeDashoffset={strokeDashoffset}
className="transition-all duration-100"
/>
{/* Glow effect */}
<circle
cx="64"
cy="64"
r="58"
fill="none"
stroke={accent}
strokeWidth="8"
strokeLinecap="round"
strokeDasharray={circumference}
strokeDashoffset={strokeDashoffset}
opacity="0.3"
filter="blur(4px)"
/>
</svg>
<div className="absolute inset-0 flex flex-col items-center justify-center">
<span className="font-mono text-3xl font-bold" style={{ color: text }}>
{Math.floor(progress)}%
</span>
</div>
</div>
);
};
/**
* Linear Bar Progress
*/
const LinearProgress: React.FC<{
progress: number;
accent: string;
}> = ({ progress, accent }) => (
<div className="w-64 h-1 bg-white/10 rounded-full overflow-hidden">
<div
className="h-full rounded-full transition-all duration-100"
style={{
width: `${progress}%`,
background: accent,
boxShadow: `0 0 20px ${accent}`,
}}
/>
</div>
);
// =============================================================================
// CONSOLE LOG DISPLAY
// =============================================================================
const BootConsole: React.FC<{
messages: string[];
currentIndex: number;
accent: string;
muted: string;
}> = ({ messages, currentIndex, accent, muted }) => (
<div
className="font-mono text-sm max-w-md mx-auto p-4 rounded-lg"
style={{ background: 'rgba(0,0,0,0.5)', border: '1px solid rgba(255,255,255,0.1)' }}
>
{messages.slice(0, currentIndex + 1).map((msg, i) => (
<div
key={i}
className="mb-1 transition-colors"
style={{ color: i === currentIndex ? accent : muted }}
>
<span style={{ color: muted }}>[{String(i).padStart(2, '0')}]</span> {msg}
</div>
))}
<span
className="inline-block w-2 h-4 animate-pulse ml-1"
style={{ background: accent }}
/>
</div>
);
// =============================================================================
// MAIN PRELOADER COMPONENT
// =============================================================================
export const CinematicPreloader: React.FC<PreloaderProps> = ({
onComplete,
theme = 'cyber',
accentColor,
bootMessages = DEFAULT_BOOT_MESSAGES,
duration = 3000,
}) => {
const [progress, setProgress] = useState(0);
const [currentLog, setCurrentLog] = useState(0);
const [complete, setComplete] = useState(false);
const colors = THEMES[theme];
const accent = accentColor || colors.accent;
const scrambledText = useTextScramble('SYSTEM READY', complete, 30);
// Progress simulation
useEffect(() => {
const interval = setInterval(() => {
setProgress(prev => {
if (prev >= 100) {
clearInterval(interval);
setComplete(true);
setTimeout(onComplete, 800);
return 100;
}
return Math.min(prev + Math.random() * (100 / (duration / 50)), 100);
});
}, 50);
return () => clearInterval(interval);
}, [onComplete, duration]);
// Log progression
useEffect(() => {
const logInterval = duration / bootMessages.length;
const interval = setInterval(() => {
setCurrentLog(prev => Math.min(prev + 1, bootMessages.length - 1));
}, logInterval);
return () => clearInterval(interval);
}, [bootMessages.length, duration]);
// Render canvas based on theme
const renderCanvas = () => {
switch (theme) {
case 'matrix':
return <MatrixCanvas accent={accent} />;
default:
return <DataGridCanvas accent={accent} />;
}
};
return (
<motion.div
className="fixed inset-0 z-50 flex flex-col items-center justify-center"
style={{ backgroundColor: colors.background }}
exit={{ opacity: 0 }}
transition={{ duration: 0.5 }}
>
{renderCanvas()}
<div className="relative z-10 flex flex-col items-center gap-8">
{/* Progress Indicator */}
<CircularProgress progress={progress} accent={accent} text={colors.text} />
{/* Status Text */}
{complete && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="font-mono text-lg tracking-wider"
style={{ color: accent }}
>
{scrambledText}
</motion.div>
)}
{/* Boot Console */}
<BootConsole
messages={bootMessages}
currentIndex={currentLog}
accent={accent}
muted={colors.muted}
/>
</div>
{/* Scanlines overlay */}
<div
className="absolute inset-0 pointer-events-none"
style={{
background: `repeating-linear-gradient(
0deg,
transparent 0px,
transparent 2px,
rgba(0,0,0,0.1) 2px,
rgba(0,0,0,0.1) 4px
)`,
}}
/>
</motion.div>
);
};
// =============================================================================
// USAGE EXAMPLE
// =============================================================================
/*
import { CinematicPreloader } from './templates/preloader';
function App() {
const [loading, setLoading] = useState(true);
return (
<>
<AnimatePresence mode="wait">
{loading && (
<CinematicPreloader
onComplete={() => setLoading(false)}
theme="cyber"
accentColor="#ff4d00"
bootMessages={[
'INITIALIZING ENGINE...',
'LOADING ASSETS...',
'CONNECTING TO SERVER...',
'READY',
]}
duration={3000}
/>
)}
</AnimatePresence>
{!loading && <MainApp />}
</>
);
}
*/
export default CinematicPreloader;