From a02f362fb05fc9d1db1d8e101c2f1070e2a841f7 Mon Sep 17 00:00:00 2001 From: ZacharyZcR Date: Sun, 9 Nov 2025 21:11:06 +0800 Subject: [PATCH] feat: Add breathe light effect as alternate loading animation with random selection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement dual-animation system with random selection: Breathe animation features: - Dynamic pulsing glow effect with 5-layer light halo - Sequential letter appearance with elastic bounce - Continuous floating animation for letters - Circular pulse rings expanding outward (3 colors: blue, purple, cyan) - Orbiting light dots on circular path (4 dots) - Particle burst effect in 8 directions - Text scaling with breathing rhythm Technical details: - 50/50 random selection between Glitch and Breathe on each load - Breathe cycle: 2.5s with smooth easing - Letter entrance: 0.8s with cubic-bezier bounce - Circular pulse rings to avoid visual artifacts - Optimized animation timing for smooth performance Visual improvements: - Stronger glow intensity (up to 150px radius) - Brightness variation (1.1 to 1.3) - Multi-colored effects (blue, purple, cyan gradient) - Smooth particle dispersal - Professional breathing rhythm 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/ui/components/LoadingOverlay.tsx | 300 +++++++++++++++++++++++++-- 1 file changed, 277 insertions(+), 23 deletions(-) diff --git a/src/ui/components/LoadingOverlay.tsx b/src/ui/components/LoadingOverlay.tsx index 448ed382..c619cd0a 100644 --- a/src/ui/components/LoadingOverlay.tsx +++ b/src/ui/components/LoadingOverlay.tsx @@ -20,11 +20,15 @@ export function LoadingOverlay({ }: LoadingOverlayProps) { const [isShowing, setIsShowing] = useState(false); const [isFadingOut, setIsFadingOut] = useState(false); + const [animationType, setAnimationType] = useState<'glitch' | 'breathe'>('glitch'); const showStartTimeRef = useRef(null); const minDurationTimerRef = useRef(null); useEffect(() => { if (visible) { + // Randomly choose animation type + setAnimationType(Math.random() > 0.5 ? 'glitch' : 'breathe'); + // Start showing immediately setIsShowing(true); setIsFadingOut(false); @@ -439,6 +443,198 @@ export function LoadingOverlay({ transform: translateX(0); } } + + /* Breathe Animation Styles */ + @keyframes breathe-glow { + 0%, 100% { + text-shadow: + 0 0 20px rgba(59, 130, 246, 0.8), + 0 0 40px rgba(59, 130, 246, 0.6), + 0 0 60px rgba(59, 130, 246, 0.4), + 0 0 80px rgba(59, 130, 246, 0.3), + 0 0 100px rgba(59, 130, 246, 0.2); + filter: brightness(1.1); + transform: scale(1); + } + 50% { + text-shadow: + 0 0 30px rgba(59, 130, 246, 1), + 0 0 60px rgba(59, 130, 246, 0.8), + 0 0 90px rgba(59, 130, 246, 0.6), + 0 0 120px rgba(59, 130, 246, 0.4), + 0 0 150px rgba(59, 130, 246, 0.3); + filter: brightness(1.3); + transform: scale(1.05); + } + } + + @keyframes letter-appear { + 0% { + opacity: 0; + transform: translateY(30px) scale(0.5); + filter: blur(10px); + } + 60% { + transform: translateY(-5px) scale(1.05); + } + 100% { + opacity: 1; + transform: translateY(0) scale(1); + filter: blur(0); + } + } + + @keyframes letter-float { + 0%, 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-8px); + } + } + + .breathe-container { + position: relative; + } + + .breathe-text { + position: relative; + color: #fff; + animation: breathe-glow 2.5s ease-in-out infinite; + } + + .breathe-text .letter { + display: inline-block; + opacity: 0; + animation: + letter-appear 0.8s cubic-bezier(0.34, 1.56, 0.64, 1) forwards, + letter-float 3s ease-in-out infinite; + } + + .breathe-text .letter:nth-child(1) { + animation-delay: 0s, 0.6s; + } + .breathe-text .letter:nth-child(2) { + animation-delay: 0.08s, 0.68s; + } + .breathe-text .letter:nth-child(3) { + animation-delay: 0.16s, 0.76s; + } + .breathe-text .letter:nth-child(4) { + animation-delay: 0.24s, 0.84s; + } + .breathe-text .letter:nth-child(5) { + animation-delay: 0.32s, 0.92s; + } + .breathe-text .letter:nth-child(6) { + animation-delay: 0.4s, 1s; + } + + @keyframes pulse-ring { + 0% { + transform: scale(0.8); + opacity: 0; + border-width: 3px; + } + 50% { + opacity: 0.6; + } + 100% { + transform: scale(1.5); + opacity: 0; + border-width: 0px; + } + } + + .pulse-ring { + position: absolute; + inset: -30px; + border: 2px solid rgba(59, 130, 246, 0.6); + border-radius: 50%; + animation: pulse-ring 2.5s ease-out infinite; + pointer-events: none; + } + + .pulse-ring:nth-child(2) { + animation-delay: 0.8s; + border-color: rgba(139, 92, 246, 0.5); + } + + .pulse-ring:nth-child(3) { + animation-delay: 1.6s; + border-color: rgba(6, 182, 212, 0.5); + } + + @keyframes orbit-dots { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } + } + + .orbit-container { + position: absolute; + inset: -60px; + animation: orbit-dots 8s linear infinite; + pointer-events: none; + } + + .orbit-dot { + position: absolute; + width: 6px; + height: 6px; + background: radial-gradient(circle, rgba(59, 130, 246, 1) 0%, rgba(59, 130, 246, 0.3) 100%); + border-radius: 50%; + box-shadow: 0 0 10px rgba(59, 130, 246, 0.8); + } + + .orbit-dot:nth-child(1) { top: 0; left: 50%; transform: translateX(-50%); } + .orbit-dot:nth-child(2) { top: 50%; right: 0; transform: translateY(-50%); } + .orbit-dot:nth-child(3) { bottom: 0; left: 50%; transform: translateX(-50%); } + .orbit-dot:nth-child(4) { top: 50%; left: 0; transform: translateY(-50%); } + + @keyframes particle-float { + 0% { + transform: translate(0, 0) scale(0); + opacity: 0; + } + 10% { + opacity: 1; + } + 90% { + opacity: 1; + } + 100% { + transform: translate(var(--tx), var(--ty)) scale(1); + opacity: 0; + } + } + + .particles { + position: absolute; + inset: 0; + pointer-events: none; + } + + .particle { + position: absolute; + width: 4px; + height: 4px; + background: radial-gradient(circle, rgba(59, 130, 246, 1) 0%, transparent 70%); + border-radius: 50%; + animation: particle-float 3s ease-out infinite; + } + + .particle:nth-child(1) { left: 50%; top: 50%; --tx: -80px; --ty: -80px; animation-delay: 0s; } + .particle:nth-child(2) { left: 50%; top: 50%; --tx: 80px; --ty: -80px; animation-delay: 0.3s; } + .particle:nth-child(3) { left: 50%; top: 50%; --tx: -80px; --ty: 80px; animation-delay: 0.6s; } + .particle:nth-child(4) { left: 50%; top: 50%; --tx: 80px; --ty: 80px; animation-delay: 0.9s; } + .particle:nth-child(5) { left: 50%; top: 50%; --tx: 0px; --ty: -100px; animation-delay: 0.15s; } + .particle:nth-child(6) { left: 50%; top: 50%; --tx: 0px; --ty: 100px; animation-delay: 0.45s; } + .particle:nth-child(7) { left: 50%; top: 50%; --tx: -100px; --ty: 0px; animation-delay: 0.75s; } + .particle:nth-child(8) { left: 50%; top: 50%; --tx: 100px; --ty: 0px; animation-delay: 1.05s; } `} @@ -450,34 +646,92 @@ export function LoadingOverlay({ )} style={{ backgroundColor: backgroundColor || "rgba(0, 0, 0, 0.92)" }} > -
-
+ {animationType === 'glitch' ? ( + <> +
+
-
-
- {/* TERMIX Glitch Text */} -
- TERMIX +
+
+ {/* TERMIX Glitch Text */} +
+ TERMIX +
+ + {/* Scan line effect */} +
+
+ + {message && ( +
+

+ {message} +

+
+ )}
+ + ) : ( + <> +
+
+ {/* Pulse rings */} +
+
+
- {/* Scan line effect */} -
-
+ {/* Orbiting dots */} +
+
+
+
+
+
- {message && ( -
-

- {message} -

+ {/* Particles */} +
+
+
+
+
+
+
+
+
+
+ + {/* TERMIX Breathe Text */} +
+ T + E + R + M + I + X +
+
+ + {message && ( +
+

+ {message} +

+
+ )}
- )} -
+ + )}
);