/** * AriaViewCanvas — die pannbare Flaeche, auf der ARIAs komponierte Ansicht * (aria_view) MATERIALISIERT: Orb oben, darunter die Karten. Erscheint als * Overlay ueber dem Chat, sobald ARIA present_view aufruft ("sag was → Orb denkt * → Karte fliegt rein"). Der erste, greifbare Vorgeschmack aufs generative * Cockpit (M1). * * Bedienung (NoMachine-Prinzip): 2-Finger halten + schieben bewegt die Welt, * Pinch zoomt. Ein-Finger-Touch geht an die Karten durch (Scrollen). Die Welt * traegt gerenderte/gestreamte Inhalte — interaktive native Panels rasten * spaeter bei Scale 1 ein (Chat bleibt separat darunter). * * Geraete-agnostisch gehalten: liest nur die ViewSpec, damit ein spaeterer Web-/ * AR-Renderer dieselbe Spec konsumieren kann. */ import React from 'react'; import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import Animated, { FadeInDown, useAnimatedStyle, useSharedValue, withTiming, } from 'react-native-reanimated'; import { Gesture, GestureDetector } from 'react-native-gesture-handler'; import { ViewSpec } from '../services/ariaView'; import Orb from './Orb'; import CardView from './CardView'; const MIN_SCALE = 0.5; const MAX_SCALE = 3; interface Props { view: ViewSpec; onClose: () => void; } const AriaViewCanvas: React.FC = ({ view, onClose }) => { const tx = useSharedValue(0); const ty = useSharedValue(0); const scale = useSharedValue(1); const savedTx = useSharedValue(0); const savedTy = useSharedValue(0); const savedScale = useSharedValue(1); const pan = Gesture.Pan() .minPointers(2) .maxPointers(2) .onUpdate((e) => { tx.value = savedTx.value + e.translationX; ty.value = savedTy.value + e.translationY; }) .onEnd(() => { savedTx.value = tx.value; savedTy.value = ty.value; }); const pinch = Gesture.Pinch() .onUpdate((e) => { const next = savedScale.value * e.scale; scale.value = Math.max(MIN_SCALE, Math.min(MAX_SCALE, next)); }) .onEnd(() => { savedScale.value = scale.value; }); const composed = Gesture.Simultaneous(pan, pinch); const worldStyle = useAnimatedStyle(() => ({ transform: [ { translateX: tx.value }, { translateY: ty.value }, { scale: scale.value }, ], })); const resetCamera = () => { tx.value = withTiming(0); ty.value = withTiming(0); scale.value = withTiming(1); savedTx.value = 0; savedTy.value = 0; savedScale.value = 1; }; const cards = Array.isArray(view.cards) ? view.cards : []; return ( {!!view.title && {view.title}} {cards.map((c, i) => ( ))} {/* Steuerung — ausserhalb des Transforms, immer bei Scale 1 bedienbar */} 2 Finger: schieben · Pinch: zoomen ); }; const styles = StyleSheet.create({ overlay: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(6,6,16,0.94)', zIndex: 50, }, world: { ...StyleSheet.absoluteFillObject, alignItems: 'center', paddingTop: 48, paddingHorizontal: 18, }, orbWrap: { marginTop: 8, marginBottom: 6 }, worldTitle: { color: '#C9C9FF', fontSize: 18, fontWeight: '700', marginBottom: 4, textAlign: 'center', }, cards: { width: '100%', maxWidth: 560 }, topBar: { position: 'absolute', top: 10, right: 12, flexDirection: 'row', }, iconBtn: { width: 40, height: 40, borderRadius: 20, marginLeft: 10, alignItems: 'center', justifyContent: 'center', backgroundColor: 'rgba(30,30,60,0.9)', borderWidth: 1, borderColor: 'rgba(123,92,255,0.4)', }, icon: { color: '#C9C9FF', fontSize: 18 }, hintWrap: { position: 'absolute', bottom: 14, alignSelf: 'center', }, hint: { color: '#6A6A90', fontSize: 12, }, }); export default AriaViewCanvas;