feat: 初始提交(仅核心代码,已排除大文件)
This commit is contained in:
@@ -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',
|
||||
},
|
||||
});
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
@@ -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%',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user