feat: 初始提交(仅核心代码,已排除大文件)

This commit is contained in:
yangxiangyuan
2026-07-08 10:29:50 +08:00
commit be7c5842f1
623 changed files with 332897 additions and 0 deletions
+143
View File
@@ -0,0 +1,143 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import {
Animated,
Easing,
LayoutChangeEvent,
StyleProp,
StyleSheet,
Text,
TextProps,
TextStyle,
View,
ViewStyle,
} from 'react-native';
interface MarqueeTextProps extends Omit<TextProps, 'style'> {
text: string;
enabled?: boolean;
style?: StyleProp<TextStyle>;
containerStyle?: StyleProp<ViewStyle>;
gap?: number;
speed?: number;
initialDelayMs?: number;
}
export const MarqueeText: React.FC<MarqueeTextProps> = ({
text,
enabled = false,
style,
containerStyle,
gap = 40,
speed = 28,
initialDelayMs = 1200,
...textProps
}) => {
const translateX = useRef(new Animated.Value(0)).current;
const animationRef = useRef<Animated.CompositeAnimation | null>(null);
const [containerWidth, setContainerWidth] = useState(0);
const [textWidth, setTextWidth] = useState(0);
const normalizedText = useMemo(() => String(text || ''), [text]);
const shouldAnimate = enabled && containerWidth > 0 && textWidth > containerWidth + 4;
const distance = Math.max(0, textWidth + gap);
const durationMs = Math.max(6000, Math.round((distance / Math.max(speed, 1)) * 1000));
useEffect(() => {
animationRef.current?.stop();
translateX.stopAnimation();
translateX.setValue(0);
if (!shouldAnimate || !distance) {
return () => {
animationRef.current?.stop();
};
}
const sequence = Animated.loop(
Animated.sequence([
Animated.delay(initialDelayMs),
Animated.timing(translateX, {
toValue: -distance,
duration: durationMs,
easing: Easing.linear,
useNativeDriver: true,
}),
Animated.delay(700),
Animated.timing(translateX, {
toValue: 0,
duration: 0,
useNativeDriver: true,
}),
])
);
animationRef.current = sequence;
sequence.start();
return () => {
sequence.stop();
animationRef.current = null;
translateX.stopAnimation();
translateX.setValue(0);
};
}, [distance, durationMs, initialDelayMs, shouldAnimate, translateX]);
const handleContainerLayout = (event: LayoutChangeEvent) => {
const nextWidth = Math.round(event.nativeEvent.layout.width || 0);
if (nextWidth !== containerWidth) {
setContainerWidth(nextWidth);
}
};
const handleTextLayout = (event: LayoutChangeEvent) => {
const nextWidth = Math.round(event.nativeEvent.layout.width || 0);
if (nextWidth !== textWidth) {
setTextWidth(nextWidth);
}
};
return (
<View style={[styles.container, containerStyle]} onLayout={handleContainerLayout}>
<Text
{...textProps}
style={[style, styles.measureText]}
onLayout={handleTextLayout}
numberOfLines={1}
>
{normalizedText}
</Text>
{shouldAnimate ? (
<Animated.View style={[styles.track, { transform: [{ translateX }] }]}>
<Text {...textProps} style={style} numberOfLines={1}>
{normalizedText}
</Text>
<View style={{ width: gap }} />
<Text {...textProps} style={style} numberOfLines={1}>
{normalizedText}
</Text>
</Animated.View>
) : (
<Text {...textProps} style={style} numberOfLines={1} ellipsizeMode="tail">
{normalizedText}
</Text>
)}
</View>
);
};
const styles = StyleSheet.create({
container: {
width: '100%',
overflow: 'hidden',
},
track: {
flexDirection: 'row',
alignItems: 'center',
},
measureText: {
position: 'absolute',
opacity: 0,
left: 0,
top: -9999,
},
});
@@ -0,0 +1,282 @@
import React from 'react';
import { View, Text, TouchableOpacity, StyleSheet, PanResponder } from 'react-native';
import Svg, { Circle, Path, Polyline, Text as SvgText } from 'react-native-svg';
import { theme } from '../theme';
import { RepeatMode } from '../types';
interface PlayerControlsProps {
isPlaying: boolean;
isShuffle: boolean;
repeatMode: RepeatMode;
position: number;
duration: number;
onPlayPause: () => void;
onNext: () => void;
onPrev: () => void;
onShuffle: () => void;
onRepeat: () => void;
onSeek: (position: number) => void;
}
const formatTime = (seconds: number): string => {
if (isNaN(seconds) || seconds < 0) return '0:00';
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
};
const PlayIcon = ({ size = 24 }: { size?: number }) => (
<Svg width={size} height={size} viewBox="0 0 24 24" fill={theme.colors.background}>
<Path d="M8 5v14l11-7z" />
</Svg>
);
const PauseIcon = ({ size = 24 }: { size?: number }) => (
<Svg width={size} height={size} viewBox="0 0 24 24" fill={theme.colors.background}>
<Path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" />
</Svg>
);
const NextIcon = ({ size = 22 }: { size?: number }) => (
<Svg width={size} height={size} viewBox="0 0 24 24" fill={theme.colors.textSecondary}>
<Path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z" />
</Svg>
);
const PrevIcon = ({ size = 22 }: { size?: number }) => (
<Svg width={size} height={size} viewBox="0 0 24 24" fill={theme.colors.textSecondary}>
<Path d="M6 6h2v12H6V6zm3.5 6l8.5 6V6l-8.5 6z" />
</Svg>
);
const ShuffleIcon = ({ active = false }: { active?: boolean }) => (
<Svg width={18} height={18} viewBox="0 0 24 24" fill="none" stroke={active ? theme.colors.primary : theme.colors.textSecondary} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
<Polyline points="16 1 21 3 21 8" />
<Path d="M4 4 C8 4 10 8 12 12 C14 16 16 20 20 20" />
<Polyline points="16 23 21 21 21 18" />
<Path d="M4 20 C8 20 10 16 12 12 C14 8 16 4 20 4" />
</Svg>
);
const RepeatIcon = ({ mode }: { mode: RepeatMode }) => {
const active = mode !== 'none';
const color = active ? theme.colors.primary : theme.colors.textSecondary;
const badgeFill = mode === 'none' ? 'transparent' : theme.colors.primary;
const badgeStroke = mode === 'none' ? 'transparent' : theme.colors.background;
return (
<Svg width={18} height={18} viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
<Polyline points="17 1 21 5 17 9" />
<Path d="M3 11V9a4 4 0 0 1 4-4h14" />
<Polyline points="7 23 3 19 7 15" />
<Path d="M21 13v2a4 4 0 0 1-4 4H3" />
{mode !== 'none' ? <Circle cx="18" cy="18" r="4.4" fill={badgeFill} stroke={badgeStroke} strokeWidth={1.2} /> : null}
{mode === 'one' ? (
<SvgText
x="18"
y="19.4"
textAnchor="middle"
fontSize="6.5"
fontWeight="700"
fill={theme.colors.background}
stroke="none"
>
1
</SvgText>
) : null}
{mode === 'all' ? (
<>
<Circle cx="16.4" cy="18" r="0.9" fill={theme.colors.background} stroke="none" />
<Circle cx="19.6" cy="18" r="0.9" fill={theme.colors.background} stroke="none" />
</>
) : null}
</Svg>
);
};
export const PlayerControls: React.FC<PlayerControlsProps> = ({
isPlaying,
isShuffle,
repeatMode,
position,
duration,
onPlayPause,
onNext,
onPrev,
onShuffle,
onRepeat,
onSeek,
}) => {
const progress = duration > 0 ? position / duration : 0;
const progressBarRef = React.useRef<View>(null);
const [isDragging, setIsDragging] = React.useState(false);
const [dragProgress, setDragProgress] = React.useState(progress);
const handleSeekTo = (newProgress: number) => {
if (duration > 0) {
onSeek(newProgress * duration);
}
};
const panResponder = React.useMemo(
() =>
PanResponder.create({
onStartShouldSetPanResponder: () => true,
onStartShouldSetPanResponderCapture: () => true,
onMoveShouldSetPanResponder: () => true,
onMoveShouldSetPanResponderCapture: () => true,
onPanResponderGrant: () => {
setIsDragging(true);
},
onPanResponderMove: (evt) => {
// 测量进度条宽度
progressBarRef.current?.measure((x, y, width) => {
if (width > 0) {
const newProgress = Math.max(0, Math.min(1, evt.nativeEvent.locationX / width));
setDragProgress(newProgress);
}
});
},
onPanResponderRelease: (evt) => {
setIsDragging(false);
progressBarRef.current?.measure((x, y, width) => {
if (width > 0) {
const newProgress = Math.max(0, Math.min(1, evt.nativeEvent.locationX / width));
handleSeekTo(newProgress);
}
});
},
onPanResponderTerminate: (evt) => {
setIsDragging(false);
progressBarRef.current?.measure((x, y, width) => {
if (width > 0) {
const newProgress = Math.max(0, Math.min(1, evt.nativeEvent.locationX / width));
handleSeekTo(newProgress);
}
});
},
}),
[duration]
);
const currentProgress = isDragging ? dragProgress : progress;
const thumbPosition = currentProgress * 100;
return (
<View style={styles.container}>
<View style={styles.glassBar}>
{/* Progress bar */}
<View style={styles.progressWrap}>
<View style={styles.timeRow}>
<Text style={styles.timeText}>{formatTime(position)}</Text>
<Text style={styles.timeText}>{formatTime(duration)}</Text>
</View>
<View
ref={progressBarRef}
style={styles.progressTrack}
{...panResponder.panHandlers}
>
<View style={[styles.progressFill, { width: `${currentProgress * 100}%` }]} />
<View style={[styles.progressThumb, { left: `${thumbPosition}%`, transform: [{ translateX: -7 }] }]} />
</View>
</View>
{/* Main controls */}
<View style={styles.controls}>
<TouchableOpacity style={styles.ctrlBtn} onPress={onShuffle}>
<ShuffleIcon active={isShuffle} />
</TouchableOpacity>
<TouchableOpacity style={styles.ctrlBtn} onPress={() => onPrev()}>
<PrevIcon />
</TouchableOpacity>
<TouchableOpacity style={styles.playBtn} onPress={() => onPlayPause()}>
{isPlaying ? <PauseIcon /> : <PlayIcon />}
</TouchableOpacity>
<TouchableOpacity style={styles.ctrlBtn} onPress={() => onNext()}>
<NextIcon />
</TouchableOpacity>
<TouchableOpacity style={styles.ctrlBtn} onPress={() => onRepeat()}>
<RepeatIcon mode={repeatMode} />
</TouchableOpacity>
</View>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
paddingHorizontal: theme.spacing.md,
paddingBottom: theme.spacing.md,
},
glassBar: {
backgroundColor: 'rgba(10,10,30,0.65)',
borderRadius: theme.borderRadius.xl,
borderWidth: 1,
borderColor: theme.colors.border,
padding: theme.spacing.md,
},
progressWrap: {
marginBottom: theme.spacing.md,
},
progressTrack: {
height: 4,
backgroundColor: 'rgba(255,255,255,0.10)',
borderRadius: 2,
position: 'relative',
},
progressFill: {
height: '100%',
backgroundColor: theme.colors.primary,
borderRadius: 2,
},
progressThumb: {
position: 'absolute',
top: -5,
width: 14,
height: 14,
borderRadius: 7,
backgroundColor: theme.colors.primary,
shadowColor: theme.colors.primary,
shadowOffset: { width: 0, height: 0 },
shadowOpacity: 0.5,
shadowRadius: 4,
elevation: 5,
},
timeRow: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: theme.spacing.xs,
},
timeText: {
fontSize: 11,
color: theme.colors.textSecondary,
fontVariant: ['tabular-nums'],
},
controls: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: theme.spacing.lg,
},
ctrlBtn: {
padding: theme.spacing.sm,
},
playBtn: {
width: 52,
height: 52,
borderRadius: 26,
backgroundColor: theme.colors.primary,
justifyContent: 'center',
alignItems: 'center',
},
});
+87
View File
@@ -0,0 +1,87 @@
import React, { useMemo } from 'react';
import { View, StyleSheet, Dimensions } from 'react-native';
import { theme } from '../theme';
const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window');
interface Star {
x: number;
y: number;
size: number;
isGold: boolean;
}
const generateStars = (count: number, size: number, isGold: boolean = false): Star[] => {
const stars: Star[] = [];
for (let i = 0; i < count; i++) {
stars.push({
x: Math.random() * SCREEN_WIDTH,
y: Math.random() * SCREEN_HEIGHT,
size,
isGold,
});
}
return stars;
};
const StarComponent: React.FC<Star> = ({ x, y, size, isGold }) => {
return (
<View
style={[
styles.star,
{
left: x,
top: y,
width: size,
height: size,
borderRadius: size / 2,
backgroundColor: isGold ? theme.colors.primary : theme.colors.text,
opacity: 0.6 + Math.random() * 0.4,
},
]}
/>
);
};
export const Starfield: React.FC = () => {
const smallStars = useMemo(() => generateStars(30, 1), []);
const mediumStars = useMemo(() => generateStars(15, 1.5), []);
const largeStars = useMemo(() => generateStars(8, 2, true), []);
return (
<View style={styles.container}>
{/* Radial gradient background simulation */}
<View style={styles.radialGlow} />
{/* Stars */}
{smallStars.map((star, i) => (
<StarComponent key={`small-${i}`} {...star} />
))}
{mediumStars.map((star, i) => (
<StarComponent key={`medium-${i}`} {...star} />
))}
{largeStars.map((star, i) => (
<StarComponent key={`large-${i}`} {...star} />
))}
</View>
);
};
const styles = StyleSheet.create({
container: {
...StyleSheet.absoluteFillObject,
backgroundColor: theme.colors.background,
},
radialGlow: {
position: 'absolute',
top: SCREEN_HEIGHT * 0.2,
left: SCREEN_WIDTH / 2 - 150,
width: 300,
height: 300,
borderRadius: 150,
backgroundColor: theme.colors.primarySoft,
},
star: {
position: 'absolute',
},
});
+543
View File
@@ -0,0 +1,543 @@
import React from 'react';
import { Animated, Easing, View, StyleSheet } from 'react-native';
import Svg, { Path, Ellipse, Circle } from 'react-native-svg';
import { theme } from '../theme';
interface ZenCenterProps {
isPlaying: boolean;
}
const ZenFigure = () => (
<View style={styles.zenFigure}>
<Svg viewBox="0 0 200 210" style={styles.zenSvg}>
{/* Ground shadow */}
<Ellipse cx="100" cy="178" rx="44" ry="5.5" fill="rgba(232,184,109,0.08)" />
{/* Lower body - Full lotus base */}
<Ellipse cx="100" cy="158" rx="46" ry="14" fill="#c8973e" opacity={0.7} />
{/* Left knee */}
<Path
d="M 54 150 C 44 152, 36 158, 36 165 C 36 171, 42 175, 52 174 C 60 173, 68 167, 72 160 C 74 155, 70 149, 64 148 Z"
fill="#d4a045"
opacity={0.78}
/>
{/* Right knee */}
<Path
d="M 146 150 C 156 152, 164 158, 164 165 C 164 171, 158 175, 148 174 C 140 173, 132 167, 128 160 C 126 155, 130 149, 136 148 Z"
fill="#d4a045"
opacity={0.78}
/>
{/* Left foot */}
<Path
d="M 124 148 C 132 144, 142 145, 146 150 C 148 153, 145 157, 138 157 C 131 157, 124 153, 122 150 Z"
fill="#c8973e"
opacity={0.72}
/>
{/* Right foot */}
<Path
d="M 76 148 C 68 144, 58 145, 54 150 C 52 153, 55 157, 62 157 C 69 157, 76 153, 78 150 Z"
fill="#c8973e"
opacity={0.72}
/>
{/* Torso */}
<Path
d="M 72 152 C 70 140, 69 126, 70 114 C 71 102, 75 92, 82 85 C 88 79, 94 77, 100 76 C 106 77, 112 79, 118 85 C 125 92, 129 102, 130 114 C 131 126, 130 140, 128 152 C 120 157, 110 160, 100 160 C 90 160, 80 157, 72 152 Z"
fill="#e0a84e"
opacity={0.88}
/>
{/* Chest line */}
<Path
d="M 100 88 C 99 108, 99 130, 100 152"
stroke="#c8882a"
strokeWidth={0.8}
opacity={0.18}
strokeLinecap="round"
/>
{/* Left upper arm */}
<Path
d="M 70 114 C 64 116, 58 122, 56 130 C 54 136, 56 142, 62 146 C 68 148, 75 146, 78 140 C 74 136, 70 128, 70 118 Z"
fill="#d4a045"
opacity={0.82}
/>
{/* Right upper arm */}
<Path
d="M 130 114 C 136 116, 142 122, 144 130 C 146 136, 144 142, 138 146 C 132 148, 125 146, 122 140 C 126 136, 130 128, 130 118 Z"
fill="#d4a045"
opacity={0.82}
/>
{/* Left hand */}
<Path
d="M 84 154 C 80 152, 76 152, 75 155 C 74 158, 76 162, 82 163 C 88 164, 96 162, 98 158 C 98 155, 92 154, 84 154 Z"
fill="#d4a045"
opacity={0.8}
/>
{/* Right hand */}
<Path
d="M 102 152 C 98 150, 94 150, 92 153 C 90 156, 91 160, 96 162 C 102 163, 110 161, 114 157 C 116 154, 112 151, 106 151 Z"
fill="#c8973e"
opacity={0.78}
/>
{/* Thumb tips touching */}
<Ellipse cx="99" cy="153" rx="4" ry="2.5" fill="#e8b86d" opacity={0.55} />
{/* Neck */}
<Path
d="M 93 78 C 93 74, 96 70, 100 69 C 104 70, 107 74, 107 78 L 107 86 L 93 86 Z"
fill="#e0a84e"
opacity={0.88}
/>
{/* Head */}
<Path
d="M 82 54 C 82 36, 90 26, 100 25 C 110 26, 118 36, 118 54 C 118 66, 112 74, 106 77 L 100 79 L 94 77 C 88 74, 82 66, 82 54 Z"
fill="#e0a84e"
opacity={0.88}
/>
{/* Hair */}
<Path
d="M 82 50 C 82 36, 90 26, 100 25 C 110 26, 118 36, 118 50 C 112 43, 106 39, 100 38 C 94 39, 88 43, 82 50 Z"
fill="#b8812a"
opacity={0.8}
/>
{/* Face features */}
{/* Left brow */}
<Path
d="M 89 53 Q 93 51, 97 53"
stroke="#8a6020"
strokeWidth={0.8}
strokeLinecap="round"
opacity={0.35}
/>
{/* Right brow */}
<Path
d="M 103 53 Q 107 51, 111 53"
stroke="#8a6020"
strokeWidth={0.8}
strokeLinecap="round"
opacity={0.35}
/>
{/* Left eye closed */}
<Path
d="M 89 57 Q 93 61, 97 57"
stroke="#7a5010"
strokeWidth={1.4}
strokeLinecap="round"
opacity={0.65}
/>
{/* Right eye closed */}
<Path
d="M 103 57 Q 107 61, 111 57"
stroke="#7a5010"
strokeWidth={1.4}
strokeLinecap="round"
opacity={0.65}
/>
{/* Nose */}
<Path
d="M 100 62 C 99 65, 97 67, 97 68"
stroke="#8a6020"
strokeWidth={1}
strokeLinecap="round"
opacity={0.25}
/>
{/* Mouth - serene half smile */}
<Path
d="M 95 72 Q 100 75, 105 72"
stroke="#8a6020"
strokeWidth={1.2}
strokeLinecap="round"
opacity={0.3}
/>
{/* Crown chakra */}
<Circle cx="100" cy="24" r="3.5" fill="rgba(232,184,109,0.2)" />
<Circle cx="100" cy="24" r="1.8" fill="#e8b86d" opacity={0.75} />
<Circle cx="100" cy="24" r="0.8" fill="#fff" opacity={0.6} />
{/* Subtle inner light */}
<Circle cx="100" cy="110" r="12" fill="rgba(232,184,109,0.04)" />
</Svg>
</View>
);
export const ZenCenter: React.FC<ZenCenterProps> = ({ isPlaying }) => {
const orbitAnimPrimary = React.useRef(new Animated.Value(0)).current;
const orbitAnimSecondary = React.useRef(new Animated.Value(0)).current;
const starBreathPrimary = React.useRef(new Animated.Value(0)).current;
const starBreathSecondary = React.useRef(new Animated.Value(0)).current;
const solarHaloAnim = React.useRef(new Animated.Value(0)).current;
React.useEffect(() => {
orbitAnimPrimary.setValue(0);
orbitAnimSecondary.setValue(0);
starBreathPrimary.setValue(0);
starBreathSecondary.setValue(0);
solarHaloAnim.setValue(0);
const primaryLoop = Animated.loop(
Animated.timing(orbitAnimPrimary, {
toValue: 1,
duration: 18000,
easing: Easing.linear,
useNativeDriver: true,
})
);
const secondaryLoop = Animated.loop(
Animated.timing(orbitAnimSecondary, {
toValue: 1,
duration: 26000,
easing: Easing.linear,
useNativeDriver: true,
})
);
const primaryBreathLoop = Animated.loop(
Animated.sequence([
Animated.timing(starBreathPrimary, {
toValue: 1,
duration: 4200,
easing: Easing.inOut(Easing.quad),
useNativeDriver: true,
}),
Animated.timing(starBreathPrimary, {
toValue: 0,
duration: 4200,
easing: Easing.inOut(Easing.quad),
useNativeDriver: true,
}),
])
);
const secondaryBreathLoop = Animated.loop(
Animated.sequence([
Animated.timing(starBreathSecondary, {
toValue: 1,
duration: 6100,
easing: Easing.inOut(Easing.quad),
useNativeDriver: true,
}),
Animated.timing(starBreathSecondary, {
toValue: 0,
duration: 6100,
easing: Easing.inOut(Easing.quad),
useNativeDriver: true,
}),
])
);
const solarHaloLoop = Animated.loop(
Animated.timing(solarHaloAnim, {
toValue: 1,
duration: 210000,
easing: Easing.linear,
useNativeDriver: true,
})
);
primaryLoop.start();
secondaryLoop.start();
primaryBreathLoop.start();
secondaryBreathLoop.start();
solarHaloLoop.start();
return () => {
primaryLoop.stop();
secondaryLoop.stop();
primaryBreathLoop.stop();
secondaryBreathLoop.stop();
solarHaloLoop.stop();
};
}, [orbitAnimPrimary, orbitAnimSecondary, solarHaloAnim, starBreathPrimary, starBreathSecondary]);
const primaryRotate = orbitAnimPrimary.interpolate({
inputRange: [0, 1],
outputRange: ['0deg', '360deg'],
});
const secondaryRotate = orbitAnimSecondary.interpolate({
inputRange: [0, 1],
outputRange: ['0deg', '-360deg'],
});
const primaryStarOpacity = starBreathPrimary.interpolate({
inputRange: [0, 1],
outputRange: [0.45, 0.9],
});
const primaryStarScale = starBreathPrimary.interpolate({
inputRange: [0, 1],
outputRange: [0.92, 1.14],
});
const secondaryStarOpacity = starBreathSecondary.interpolate({
inputRange: [0, 1],
outputRange: [0.16, 0.55],
});
const secondaryStarScale = starBreathSecondary.interpolate({
inputRange: [0, 1],
outputRange: [0.9, 1.2],
});
const haloRotate = solarHaloAnim.interpolate({
inputRange: [0, 1],
outputRange: ['0deg', '360deg'],
});
const haloOpacity = solarHaloAnim.interpolate({
inputRange: [0, 0.12, 0.18, 0.26, 0.62, 0.72, 0.8, 1],
outputRange: [0.02, 0.03, 0.12, 0.04, 0.03, 0.11, 0.04, 0.02],
});
const haloScale = solarHaloAnim.interpolate({
inputRange: [0, 0.18, 0.62, 0.72, 1],
outputRange: [0.88, 1.06, 0.9, 1.08, 0.88],
});
return (
<View style={styles.container}>
<View style={styles.orbitWrap}>
<Animated.View
pointerEvents="none"
style={[
styles.solarHaloTrack,
{
opacity: haloOpacity,
transform: [{ rotate: haloRotate }],
},
]}
>
<Animated.View
style={[
styles.solarHalo,
{
transform: [{ scale: haloScale }],
},
]}
>
<View style={styles.solarCore} />
</Animated.View>
</Animated.View>
<Animated.View
pointerEvents="none"
style={[styles.orbitLayerPrimary, { transform: [{ rotate: primaryRotate }] }]}
>
<View style={styles.orbitRing} />
<Animated.View
style={[
styles.orbitDot1,
{
opacity: primaryStarOpacity,
transform: [{ scale: primaryStarScale }],
},
]}
/>
<Animated.View
style={[
styles.orbitDot1Trail,
{
opacity: secondaryStarOpacity,
transform: [{ scale: secondaryStarScale }],
},
]}
/>
</Animated.View>
<Animated.View
pointerEvents="none"
style={[styles.orbitLayerSecondary, { transform: [{ rotate: secondaryRotate }] }]}
>
<View style={styles.orbitRing2} />
<Animated.View
style={[
styles.orbitDot2,
{
opacity: secondaryStarOpacity,
transform: [{ scale: secondaryStarScale }],
},
]}
/>
<Animated.View
style={[
styles.orbitDot3,
{
opacity: primaryStarOpacity,
transform: [{ scale: primaryStarScale }],
},
]}
/>
</Animated.View>
{/* Zen glow */}
<View style={[styles.zenGlow, { opacity: isPlaying ? 1 : 0.55 }]} />
{/* Zen waves */}
<View style={styles.zenWaves}>
<View style={styles.zenWave} />
</View>
{/* Zen figure */}
<ZenFigure />
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
orbitWrap: {
width: 220,
height: 220,
justifyContent: 'center',
alignItems: 'center',
},
orbitLayerPrimary: {
position: 'absolute',
width: 220,
height: 220,
justifyContent: 'center',
alignItems: 'center',
},
orbitLayerSecondary: {
position: 'absolute',
width: 256,
height: 256,
justifyContent: 'center',
alignItems: 'center',
},
solarHaloTrack: {
position: 'absolute',
width: 320,
height: 320,
justifyContent: 'center',
alignItems: 'center',
},
solarHalo: {
position: 'absolute',
top: -18,
right: 24,
width: 92,
height: 92,
borderRadius: 46,
backgroundColor: 'rgba(232,184,109,0.14)',
shadowColor: theme.colors.primary,
shadowOffset: { width: 0, height: 0 },
shadowOpacity: 0.18,
shadowRadius: 24,
elevation: 8,
justifyContent: 'center',
alignItems: 'center',
},
solarCore: {
width: 26,
height: 26,
borderRadius: 13,
backgroundColor: 'rgba(255,244,214,0.28)',
},
orbitRing: {
position: 'absolute',
width: 220,
height: 220,
borderRadius: 110,
borderWidth: 1,
borderColor: 'rgba(232,184,109,0.12)',
},
orbitRing2: {
position: 'absolute',
width: 256,
height: 256,
borderRadius: 128,
borderWidth: 1,
borderColor: 'rgba(232,184,109,0.06)',
},
orbitDot1: {
position: 'absolute',
top: -3,
left: 107,
width: 6,
height: 6,
borderRadius: 3,
backgroundColor: theme.colors.primary,
opacity: 0.55,
},
orbitDot1Trail: {
position: 'absolute',
top: 26,
right: 30,
width: 3,
height: 3,
borderRadius: 1.5,
backgroundColor: theme.colors.primary,
opacity: 0.22,
},
orbitDot2: {
position: 'absolute',
bottom: -2,
left: 127,
width: 4,
height: 4,
borderRadius: 2,
backgroundColor: theme.colors.primary,
opacity: 0.3,
},
orbitDot3: {
position: 'absolute',
top: 36,
left: 18,
width: 5,
height: 5,
borderRadius: 2.5,
backgroundColor: theme.colors.primary,
opacity: 0.18,
},
zenGlow: {
position: 'absolute',
width: 160,
height: 160,
borderRadius: 80,
backgroundColor: theme.colors.primarySoft,
opacity: 0,
},
zenWaves: {
position: 'absolute',
width: 220,
height: 220,
justifyContent: 'center',
alignItems: 'center',
},
zenWave: {
position: 'absolute',
width: 160,
height: 160,
borderRadius: 80,
borderWidth: 1,
borderColor: 'rgba(232,184,109,0.08)',
},
zenFigure: {
width: 140,
height: 140,
},
zenSvg: {
width: '100%',
height: '100%',
},
});