/** * Orb — ARIAs Praesenz-Avatar (M1). Zeigt ihren Zustand (idle/listening/ * thinking/speaking/working) als pulsierender Leucht-Kern und ist das * verbindende Element ueber alle Oberflaechen (App/Web/spaeter Brille). * * Reine Optik, keine Logik — der Zustand kommt von aussen (aria_view.orb bzw. * spaeter direkt von Audio/Wake-Word-Signalen). Dependency-leicht: nur * reanimated (schon installiert), kein SVG/Gradient noetig. */ import React, { useEffect } from 'react'; import { StyleSheet, View } from 'react-native'; import Animated, { Easing, cancelAnimation, useAnimatedStyle, useSharedValue, withRepeat, withTiming, } from 'react-native-reanimated'; import { OrbState } from '../services/ariaView'; const COLORS: Record = { idle: '#3A6EA5', listening: '#00B4D8', thinking: '#7B5CFF', speaking: '#34C759', working: '#FF9500', }; interface Props { state?: OrbState; size?: number; } const Orb: React.FC = ({ state = 'idle', size = 120 }) => { const pulse = useSharedValue(1); useEffect(() => { const fast = state === 'thinking' || state === 'working'; cancelAnimation(pulse); pulse.value = 1; pulse.value = withRepeat( withTiming(fast ? 1.14 : 1.07, { duration: fast ? 620 : 1500, easing: Easing.inOut(Easing.ease), }), -1, true, ); return () => cancelAnimation(pulse); }, [state, pulse]); const animStyle = useAnimatedStyle(() => ({ transform: [{ scale: pulse.value }] })); const color = COLORS[state] || COLORS.idle; return ( ); }; const styles = StyleSheet.create({ wrap: { alignItems: 'center', justifyContent: 'center' }, glow: { position: 'absolute', opacity: 0.22 }, ring: { position: 'absolute', borderWidth: 2, opacity: 0.55 }, core: { shadowOpacity: 0.9, shadowRadius: 16, shadowOffset: { width: 0, height: 0 }, elevation: 12, }, }); export default React.memo(Orb);