diff options
| author | soryu <soryu@soryu.co> | 2025-11-15 18:00:09 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2025-11-15 18:00:09 +0000 |
| commit | 3e7b2beca1136a42700a7e1aebfe4c0fb2861a00 (patch) | |
| tree | 6c896c31820681e360e50a73839fc2284c043dea /frontend/src/components/LoadingScreen.tsx | |
| download | soryu-3e7b2beca1136a42700a7e1aebfe4c0fb2861a00.tar.gz soryu-3e7b2beca1136a42700a7e1aebfe4c0fb2861a00.zip | |
Initial commit
Diffstat (limited to 'frontend/src/components/LoadingScreen.tsx')
| -rw-r--r-- | frontend/src/components/LoadingScreen.tsx | 129 |
1 files changed, 129 insertions, 0 deletions
diff --git a/frontend/src/components/LoadingScreen.tsx b/frontend/src/components/LoadingScreen.tsx new file mode 100644 index 0000000..5f33d00 --- /dev/null +++ b/frontend/src/components/LoadingScreen.tsx @@ -0,0 +1,129 @@ +import React, { useEffect, useState, useRef } from 'react' + +type Props = { + onComplete: () => void +} + +export const LoadingScreen: React.FC<Props> = ({ onComplete }) => { + const [fadeOut, setFadeOut] = useState(false) + const [mounted, setMounted] = useState(true) + const [subtitle, setSubtitle] = useState('') + const [animatedText, setAnimatedText] = useState('') + const subtitleLoaded = useRef(false) + + useEffect(() => { + console.log('Loading screen mounted - starting timer...') + + // Set fixed subtitle and animate it + if (!subtitleLoaded.current) { + subtitleLoaded.current = true + + const fixedSubtitle = 'Whisper of the Heart' + setSubtitle(fixedSubtitle) + + // Animate text character by character + let currentIndex = 0 + const animateText = () => { + if (currentIndex <= fixedSubtitle.length) { + setAnimatedText(fixedSubtitle.slice(0, currentIndex)) + currentIndex++ + setTimeout(animateText, 50) // 50ms delay between characters + } + } + + // Start animation after heart appears (1000ms = 0.2s heart delay + 0.8s heart animation) + setTimeout(animateText, 1000) + } + + // Start fade after 4 seconds (longer to show ray spinning) + const fadeTimer = setTimeout(() => { + console.log('4 seconds passed - Starting fade out...') + setFadeOut(true) + }, 4000) + + // Complete after fade finishes (4s show + 1s fade = 5s total) + const completeTimer = setTimeout(() => { + console.log('5 seconds passed - Loading screen completing...') + setMounted(false) + onComplete() + }, 5000) + + return () => { + console.log('Cleaning up timers...') + clearTimeout(fadeTimer) + clearTimeout(completeTimer) + } + }, []) // Empty dependency array - run once on mount + + const handleClick = () => { + console.log('Loading screen clicked, completing early...') + setFadeOut(true) + setTimeout(() => { + setMounted(false) + onComplete() + }, 1000) + } + + console.log('LoadingScreen render - fadeOut:', fadeOut, 'mounted:', mounted) + + if (!mounted) return null + + return ( + <div className={`loading-screen ${fadeOut ? 'fade-out' : ''}`} onClick={handleClick}> + <div className="loading-logo"> + <div className="heart-container"> + <div className="sun-rays loading-animate-rays"> + <svg viewBox="0 0 100 100" className="rays-svg"> + {/* Rising Sun flag style rays - triangular rays extending to edges */} + <g transform="translate(50, 50)"> + {[...Array(16)].map((_, i) => { + const angle = (i * 22.5); + const nextAngle = ((i + 1) * 22.5); + const rayWidth = 11.25; // Half the angle between rays for triangular shape + + // Create triangular ray path + const startAngle = angle - rayWidth; + const endAngle = angle + rayWidth; + + const innerRadius = 0; // Start from center (heart center) + const outerRadius = 70; // Extend to screen edge + + const x1 = Math.cos(startAngle * Math.PI / 180) * innerRadius; + const y1 = Math.sin(startAngle * Math.PI / 180) * innerRadius; + const x2 = Math.cos(endAngle * Math.PI / 180) * innerRadius; + const y2 = Math.sin(endAngle * Math.PI / 180) * innerRadius; + const x3 = Math.cos(startAngle * Math.PI / 180) * outerRadius; + const y3 = Math.sin(startAngle * Math.PI / 180) * outerRadius; + const x4 = Math.cos(endAngle * Math.PI / 180) * outerRadius; + const y4 = Math.sin(endAngle * Math.PI / 180) * outerRadius; + + return ( + <polygon + key={i} + points={`${x1},${y1} ${x2},${y2} ${x4},${y4} ${x3},${y3}`} + fill={i % 2 === 0 ? "#8B0000" : "#000000"} + opacity="0.9" + /> + ); + })} + </g> + </svg> + </div> + <div className="heart-outline loading-animate-heart"> + <svg viewBox="0 0 100 100" className="heart-svg"> + <path d="M50,85 C50,85 10,60 10,35 C10,18 20,8 35,8 C42,8 50,13 50,25 C50,13 58,8 65,8 C80,8 90,18 90,35 C90,60 50,85 50,85 Z" + fill="#8B0000" + stroke="#8B0000" + strokeWidth="2"/> + </svg> + </div> + <div className="logo-text loading-animate-text">soryu</div> + <div className="loading-subtitle loading-animate-subtitle">{animatedText}</div> + <div className="loading-dots loading-animate-text"> + <span>.</span><span>.</span><span>.</span> + </div> + </div> + </div> + </div> + ) +}
\ No newline at end of file |
