413 lines
12 KiB
TypeScript

import { useEffect, useRef } from 'react';
import { Animated, Dimensions, Image, Text, View } from 'react-native';
import Svg, { Defs, RadialGradient, Rect, Stop } from 'react-native-svg';
import { useTranslation } from 'react-i18next';
// Phase-Timings (ms ab Mount) — 1:1 portiert aus apps/rebreak/app/components/AppSplash.vue
const T_GLOW = 0;
const T_NAME = 300;
const T_LOGO = 700;
const T_PULSE = 1100;
const T_TAGLINE = 1300;
const T_SUB = 1700;
const T_HOLD_END = 3200;
const T_LEAVE_DUR = 500;
const { width: SW, height: SH } = Dimensions.get('window');
type ParticleConfig = {
size: number;
top?: number;
bottom?: number;
left?: number;
right?: number;
duration: number;
delay: number;
};
const PARTICLES: ParticleConfig[] = [
{ size: 180, top: -40, left: -60, duration: 7000, delay: 0 },
{ size: 120, bottom: SH * 0.1, right: -30, duration: 9000, delay: 1500 },
{ size: 80, top: SH * 0.35, left: SW * 0.08, duration: 11000, delay: 800 },
{ size: 60, bottom: SH * 0.2, left: SW * 0.2, duration: 8000, delay: 2200 },
{ size: 100, top: SH * 0.15, right: SW * 0.1, duration: 10000, delay: 400 },
];
function Particle({ config }: { config: ParticleConfig }) {
const translateY = useRef(new Animated.Value(0)).current;
const scale = useRef(new Animated.Value(1)).current;
const opacity = useRef(new Animated.Value(0.6)).current;
useEffect(() => {
const animate = () => {
Animated.loop(
Animated.sequence([
Animated.parallel([
Animated.timing(translateY, {
toValue: 18,
duration: config.duration,
useNativeDriver: true,
}),
Animated.timing(scale, {
toValue: 1.1,
duration: config.duration,
useNativeDriver: true,
}),
Animated.timing(opacity, {
toValue: 1,
duration: config.duration,
useNativeDriver: true,
}),
]),
Animated.parallel([
Animated.timing(translateY, {
toValue: 0,
duration: config.duration,
useNativeDriver: true,
}),
Animated.timing(scale, {
toValue: 1,
duration: config.duration,
useNativeDriver: true,
}),
Animated.timing(opacity, {
toValue: 0.6,
duration: config.duration,
useNativeDriver: true,
}),
]),
]),
).start();
};
const t = setTimeout(animate, config.delay);
return () => clearTimeout(t);
}, [config, translateY, scale, opacity]);
return (
<Animated.View
pointerEvents="none"
style={{
position: 'absolute',
width: config.size,
height: config.size,
borderRadius: config.size / 2,
backgroundColor: 'rgba(99, 102, 241, 0.12)',
top: config.top,
bottom: config.bottom,
left: config.left,
right: config.right,
opacity,
transform: [{ translateY }, { scale }],
}}
/>
);
}
export function BrandSplash() {
const { t } = useTranslation();
// Phase-Opacity-Animationen
const containerOpacity = useRef(new Animated.Value(1)).current;
const glowCenterOpacity = useRef(new Animated.Value(0)).current;
const glowCenterScale = useRef(new Animated.Value(0.6)).current;
const glowTopOpacity = useRef(new Animated.Value(0.5)).current;
const nameOpacity = useRef(new Animated.Value(0)).current;
const nameTranslateY = useRef(new Animated.Value(12)).current;
const logoOpacity = useRef(new Animated.Value(0)).current;
const logoScale = useRef(new Animated.Value(0.82)).current;
const logoTranslateY = useRef(new Animated.Value(8)).current;
const logoPulse = useRef(new Animated.Value(1)).current;
const taglineOpacity = useRef(new Animated.Value(0)).current;
const taglineTranslateY = useRef(new Animated.Value(8)).current;
const subOpacity = useRef(new Animated.Value(0)).current;
const subTranslateY = useRef(new Animated.Value(6)).current;
const footerOpacity = useRef(new Animated.Value(0)).current;
useEffect(() => {
// Top-glow breath loop (4s alternating) — startet sofort
Animated.loop(
Animated.sequence([
Animated.timing(glowTopOpacity, {
toValue: 0.9,
duration: 2000,
useNativeDriver: true,
}),
Animated.timing(glowTopOpacity, {
toValue: 0.5,
duration: 2000,
useNativeDriver: true,
}),
]),
).start();
const ease = (toValue: number, duration: number) => ({
toValue,
duration,
useNativeDriver: true,
});
// Phase 1: glow center bloom (T=0)
Animated.parallel([
Animated.timing(glowCenterOpacity, ease(1, 900)),
Animated.timing(glowCenterScale, ease(1, 900)),
]).start();
// Phase 2: Name fade-in (T=300)
setTimeout(() => {
Animated.parallel([
Animated.timing(nameOpacity, ease(1, 600)),
Animated.timing(nameTranslateY, ease(0, 600)),
]).start();
}, T_NAME);
// Phase 3: Logo bouncy scale-in (T=700)
setTimeout(() => {
Animated.parallel([
Animated.timing(logoOpacity, ease(1, 650)),
Animated.spring(logoScale, {
toValue: 1,
useNativeDriver: true,
friction: 6,
tension: 80,
}),
Animated.timing(logoTranslateY, ease(0, 650)),
]).start();
}, T_LOGO);
// Phase 3b: Logo breathing pulse (T=1100)
setTimeout(() => {
Animated.loop(
Animated.sequence([
Animated.timing(logoPulse, {
toValue: 1.04,
duration: 1300,
useNativeDriver: true,
}),
Animated.timing(logoPulse, {
toValue: 1,
duration: 1300,
useNativeDriver: true,
}),
]),
).start();
}, T_PULSE);
// Phase 4: Tagline (T=1300)
setTimeout(() => {
Animated.parallel([
Animated.timing(taglineOpacity, ease(1, 550)),
Animated.timing(taglineTranslateY, ease(0, 550)),
]).start();
}, T_TAGLINE);
// Phase 5: Sub-text + Footer (T=1700)
setTimeout(() => {
Animated.parallel([
Animated.timing(subOpacity, ease(1, 500)),
Animated.timing(subTranslateY, ease(0, 500)),
Animated.timing(footerOpacity, ease(1, 600)),
]).start();
}, T_SUB);
// Phase 7: whole-screen fade-out (T=3200, dauert 500ms)
const fadeOutTimer = setTimeout(() => {
Animated.timing(containerOpacity, {
toValue: 0,
duration: T_LEAVE_DUR,
useNativeDriver: true,
}).start();
}, T_HOLD_END);
return () => clearTimeout(fadeOutTimer);
}, [
glowTopOpacity,
glowCenterOpacity,
glowCenterScale,
nameOpacity,
nameTranslateY,
logoOpacity,
logoScale,
logoTranslateY,
logoPulse,
taglineOpacity,
taglineTranslateY,
subOpacity,
subTranslateY,
footerOpacity,
containerOpacity,
]);
return (
<Animated.View
style={{
flex: 1,
backgroundColor: '#0f172a',
opacity: containerOpacity,
overflow: 'hidden',
}}
>
{/* Top breathing radial-gradient ellipse (#1e3a8a auf transparent) */}
<Animated.View
pointerEvents="none"
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: SH * 0.5,
opacity: glowTopOpacity,
}}
>
<Svg width="100%" height="100%">
<Defs>
<RadialGradient
id="topGlow"
cx="50%"
cy="0%"
rx="70%"
ry="100%"
fx="50%"
fy="0%"
>
<Stop offset="0%" stopColor="#1e3a8a" stopOpacity="1" />
<Stop offset="100%" stopColor="#1e3a8a" stopOpacity="0" />
</RadialGradient>
</Defs>
<Rect width="100%" height="100%" fill="url(#topGlow)" />
</Svg>
</Animated.View>
{/* Center indigo halo — bloomt rein wenn Logo erscheint */}
<Animated.View
pointerEvents="none"
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
opacity: glowCenterOpacity,
transform: [{ scale: glowCenterScale }],
}}
>
<Svg width="100%" height="100%">
<Defs>
<RadialGradient id="centerHalo" cx="50%" cy="52%" rx="55%" ry="55%">
<Stop offset="0%" stopColor="#6366f1" stopOpacity="0.22" />
<Stop offset="100%" stopColor="#6366f1" stopOpacity="0" />
</RadialGradient>
</Defs>
<Rect width="100%" height="100%" fill="url(#centerHalo)" />
</Svg>
</Animated.View>
{/* Floating particles (5 Stück) */}
{PARTICLES.map((p, i) => (
<Particle key={i} config={p} />
))}
{/* Content-Column */}
<View
style={{
flex: 1,
alignItems: 'center',
justifyContent: 'center',
gap: 20,
paddingHorizontal: 16,
}}
>
{/* App-Name */}
<Animated.Text
style={{
fontFamily: 'Nunito_800ExtraBold',
fontSize: 48,
letterSpacing: -1,
color: '#ffffff',
textAlign: 'center',
marginBottom: 8,
opacity: nameOpacity,
transform: [{ translateY: nameTranslateY }],
}}
>
{t('appHeader.appName')}
</Animated.Text>
{/* Logo (mit Pulse + Bouncy Entry) */}
<Animated.View
style={{
opacity: logoOpacity,
transform: [
{ scale: Animated.multiply(logoScale, logoPulse) as any },
{ translateY: logoTranslateY },
],
}}
>
<Image
source={require('../assets/icon.png')}
style={{
width: 160,
height: 160,
borderRadius: 28,
}}
resizeMode="contain"
/>
</Animated.View>
{/* Tagline */}
<Animated.Text
style={{
fontFamily: 'Nunito_600SemiBold',
fontSize: 20,
letterSpacing: 0.2,
color: 'rgba(255, 255, 255, 0.90)',
textAlign: 'center',
marginTop: 4,
opacity: taglineOpacity,
transform: [{ translateY: taglineTranslateY }],
}}
>
{t('splash.tagline')}
</Animated.Text>
{/* Sub-text */}
<Animated.Text
style={{
fontFamily: 'Nunito_400Regular',
fontSize: 14,
letterSpacing: 0.6,
color: 'rgba(255, 255, 255, 0.55)',
textAlign: 'center',
opacity: subOpacity,
transform: [{ translateY: subTranslateY }],
}}
>
{t('splash.subtitle')}
</Animated.Text>
</View>
{/* Footer */}
<Animated.Text
style={{
position: 'absolute',
bottom: 32,
left: 0,
right: 0,
fontFamily: 'Nunito_400Regular',
fontSize: 11,
letterSpacing: 1.5,
textTransform: 'uppercase',
color: 'rgba(255, 255, 255, 0.28)',
textAlign: 'center',
opacity: footerOpacity,
}}
>
{t('splash.madeInGermany')}
</Animated.Text>
</Animated.View>
);
}