413 lines
12 KiB
TypeScript
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>
|
|
);
|
|
}
|