added DreamVR
component for neural network-based VR visualization of chip
input dreams, integrated @react-three/fiber
and @react-three/drei
dependencies, and updated package-lock.json
Signed-off-by: Matthias Puchstein <matthias@puchstein.bayern>
This commit is contained in:
265
src/components/DreamVR.tsx
Normal file
265
src/components/DreamVR.tsx
Normal file
@@ -0,0 +1,265 @@
|
||||
import React, {useMemo, useRef} from 'react';
|
||||
import {Canvas, useFrame} from '@react-three/fiber';
|
||||
import {OrbitControls, PerspectiveCamera} from '@react-three/drei';
|
||||
import * as THREE from 'three';
|
||||
import type Dream from '../types/Dream';
|
||||
|
||||
// Neural Node component representing a synapse in the neural network
|
||||
const NeuralNode = ({position, color, scale, pulseSpeed, pulseIntensity}: {
|
||||
position: [number, number, number],
|
||||
color: string,
|
||||
scale: number,
|
||||
pulseSpeed: number,
|
||||
pulseIntensity: number
|
||||
}) => {
|
||||
const nodeRef = useRef<THREE.Mesh>(null);
|
||||
const initialScale = scale;
|
||||
|
||||
useFrame(({clock}) => {
|
||||
if (nodeRef.current) {
|
||||
// Create a pulsing effect
|
||||
const pulse = Math.sin(clock.getElapsedTime() * pulseSpeed) * pulseIntensity + 1;
|
||||
nodeRef.current.scale.set(initialScale * pulse, initialScale * pulse, initialScale * pulse);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<mesh ref={nodeRef} position={position}>
|
||||
<sphereGeometry args={[1, 32, 32]}/>
|
||||
<meshStandardMaterial
|
||||
color={color}
|
||||
emissive={color}
|
||||
emissiveIntensity={0.5}
|
||||
transparent
|
||||
opacity={0.8}
|
||||
/>
|
||||
</mesh>
|
||||
);
|
||||
};
|
||||
|
||||
// Neural Connection component representing connections between synapses
|
||||
const NeuralConnection = ({start, end, color, thickness, pulseSpeed, pulseIntensity}: {
|
||||
start: [number, number, number],
|
||||
end: [number, number, number],
|
||||
color: string,
|
||||
thickness: number,
|
||||
pulseSpeed: number,
|
||||
pulseIntensity: number
|
||||
}) => {
|
||||
const connectionRef = useRef<THREE.Mesh>(null);
|
||||
|
||||
// Create a cylinder between two points
|
||||
const direction = useMemo(() => {
|
||||
return new THREE.Vector3(
|
||||
end[0] - start[0],
|
||||
end[1] - start[1],
|
||||
end[2] - start[2]
|
||||
);
|
||||
}, [start, end]);
|
||||
|
||||
const length = useMemo(() => direction.length(), [direction]);
|
||||
|
||||
// Calculate rotation to align cylinder with direction
|
||||
const rotationMatrix = useMemo(() => {
|
||||
const normalizedDirection = direction.clone().normalize();
|
||||
const quaternion = new THREE.Quaternion();
|
||||
quaternion.setFromUnitVectors(
|
||||
new THREE.Vector3(0, 1, 0), // Default cylinder orientation
|
||||
normalizedDirection
|
||||
);
|
||||
return new THREE.Matrix4().makeRotationFromQuaternion(quaternion);
|
||||
}, [direction]);
|
||||
|
||||
// Calculate position (midpoint between start and end)
|
||||
const position = useMemo(() => {
|
||||
return [
|
||||
(start[0] + end[0]) / 2,
|
||||
(start[1] + end[1]) / 2,
|
||||
(start[2] + end[2]) / 2
|
||||
] as [number, number, number];
|
||||
}, [start, end]);
|
||||
|
||||
useFrame(({clock}) => {
|
||||
if (connectionRef.current) {
|
||||
// Create a pulsing effect for the connection
|
||||
const pulse = Math.sin(clock.getElapsedTime() * pulseSpeed +
|
||||
(start[0] + start[1] + start[2])) * pulseIntensity + 1;
|
||||
connectionRef.current.scale.set(thickness * pulse, length / 2, thickness * pulse);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<mesh
|
||||
ref={connectionRef}
|
||||
position={position}
|
||||
matrix={rotationMatrix}
|
||||
>
|
||||
<cylinderGeometry args={[thickness, thickness, length / 2, 8]}/>
|
||||
<meshStandardMaterial
|
||||
color={color}
|
||||
emissive={color}
|
||||
emissiveIntensity={0.5}
|
||||
transparent
|
||||
opacity={0.6}
|
||||
/>
|
||||
</mesh>
|
||||
);
|
||||
};
|
||||
|
||||
// Neural Network component that generates nodes and connections
|
||||
const NeuralNetwork = ({dream}: { dream: Dream }) => {
|
||||
// Generate nodes and connections based on dream data
|
||||
const nodes = useMemo(() => {
|
||||
if (dream.input.inputType !== 'chip') return [];
|
||||
|
||||
const chipInput = dream.input;
|
||||
const nodeCount = 50; // Number of nodes to generate
|
||||
const nodes = [];
|
||||
|
||||
// Use EEG data to influence node positions and properties
|
||||
for (let i = 0; i < nodeCount; i++) {
|
||||
const alphaValue = chipInput.eeg.alpha[i % chipInput.eeg.alpha.length] / 50;
|
||||
const betaValue = chipInput.eeg.beta[i % chipInput.eeg.beta.length] / 50;
|
||||
const thetaValue = chipInput.eeg.theta[i % chipInput.eeg.theta.length] / 50;
|
||||
|
||||
// Calculate position based on EEG values
|
||||
const x = (Math.random() - 0.5) * 20 + alphaValue * 10;
|
||||
const y = (Math.random() - 0.5) * 20 + betaValue * 10;
|
||||
const z = (Math.random() - 0.5) * 20 + thetaValue * 10;
|
||||
|
||||
// Determine color based on dominant wave
|
||||
let color;
|
||||
const dominantWave = Math.max(alphaValue, betaValue, thetaValue);
|
||||
if (dominantWave === alphaValue) {
|
||||
color = '#8884d8'; // Alpha - purple
|
||||
} else if (dominantWave === betaValue) {
|
||||
color = '#82ca9d'; // Beta - green
|
||||
} else {
|
||||
color = '#ffc658'; // Theta - yellow
|
||||
}
|
||||
|
||||
// Scale based on pulse data
|
||||
const scale = 0.2 + (chipInput.puls[i % chipInput.puls.length] / 200);
|
||||
|
||||
// Pulse speed and intensity based on HRV and movement
|
||||
const pulseSpeed = 1 + (chipInput.hrv[i % chipInput.hrv.length] / 35);
|
||||
const pulseIntensity = 0.1 + (chipInput.bewegung[i % chipInput.bewegung.length] * 2);
|
||||
|
||||
nodes.push({
|
||||
position: [x, y, z] as [number, number, number],
|
||||
color,
|
||||
scale,
|
||||
pulseSpeed,
|
||||
pulseIntensity
|
||||
});
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}, [dream]);
|
||||
|
||||
// Generate connections between nodes
|
||||
const connections = useMemo(() => {
|
||||
if (dream.input.inputType !== 'chip') return [];
|
||||
|
||||
const connections = [];
|
||||
const connectionCount = nodes.length * 2; // Each node connects to ~2 others
|
||||
|
||||
for (let i = 0; i < connectionCount; i++) {
|
||||
const startNodeIndex = i % nodes.length;
|
||||
// Connect to a nearby node (not completely random)
|
||||
const endNodeIndex = (startNodeIndex + 1 + Math.floor(Math.random() * 5)) % nodes.length;
|
||||
|
||||
if (startNodeIndex !== endNodeIndex) {
|
||||
connections.push({
|
||||
start: nodes[startNodeIndex].position,
|
||||
end: nodes[endNodeIndex].position,
|
||||
color: nodes[startNodeIndex].color,
|
||||
thickness: 0.05 + Math.random() * 0.05,
|
||||
pulseSpeed: nodes[startNodeIndex].pulseSpeed,
|
||||
pulseIntensity: nodes[startNodeIndex].pulseIntensity
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return connections;
|
||||
}, [nodes, dream]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Render all nodes */}
|
||||
{nodes.map((node, index) => (
|
||||
<NeuralNode
|
||||
key={`node-${index}`}
|
||||
position={node.position}
|
||||
color={node.color}
|
||||
scale={node.scale}
|
||||
pulseSpeed={node.pulseSpeed}
|
||||
pulseIntensity={node.pulseIntensity}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Render all connections */}
|
||||
{connections.map((connection, index) => (
|
||||
<NeuralConnection
|
||||
key={`connection-${index}`}
|
||||
start={connection.start}
|
||||
end={connection.end}
|
||||
color={connection.color}
|
||||
thickness={connection.thickness}
|
||||
pulseSpeed={connection.pulseSpeed}
|
||||
pulseIntensity={connection.pulseIntensity}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// Main DreamVR component
|
||||
interface DreamVRProps {
|
||||
dream: Dream;
|
||||
height?: string;
|
||||
}
|
||||
|
||||
const DreamVR: React.FC<DreamVRProps> = ({dream, height = '500px'}) => {
|
||||
// Only render for dream with chip input type
|
||||
if (dream.input.inputType !== 'chip') {
|
||||
return (
|
||||
<div className="flex items-center justify-center" style={{height}}>
|
||||
<p className="text-center text-gray-500">
|
||||
VR-Visualisierung ist nur für Träume mit Chip-Eingabe verfügbar.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{height, width: '100%'}}>
|
||||
<Canvas shadows>
|
||||
{/* Camera setup */}
|
||||
<PerspectiveCamera makeDefault position={[0, 0, 30]}/>
|
||||
|
||||
{/* Controls */}
|
||||
<OrbitControls
|
||||
enableZoom={true}
|
||||
enablePan={true}
|
||||
enableRotate={true}
|
||||
autoRotate={true}
|
||||
autoRotateSpeed={0.5}
|
||||
/>
|
||||
|
||||
{/* Lighting */}
|
||||
<ambientLight intensity={0.5}/>
|
||||
<pointLight position={[10, 10, 10]} intensity={1}/>
|
||||
<pointLight position={[-10, -10, -10]} intensity={0.5}/>
|
||||
|
||||
{/* Neural network visualization */}
|
||||
<NeuralNetwork dream={dream}/>
|
||||
|
||||
{/* Background */}
|
||||
<color attach="background" args={['#000']}/>
|
||||
</Canvas>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DreamVR;
|
@@ -85,6 +85,7 @@ export const mockDreams: Dream[] = [
|
||||
interpretation: 'Diese Szene steht für Geborgenheit und Gemeinschaft auf einer höheren Ebene. Der Sternenstaub in den Tassen symbolisiert geteilte Träume, die pastelligen Regenbögen zeigen eine optimistische Grundstimmung und Leichtigkeit.',
|
||||
image: '04.png',
|
||||
audio: '04.mp3',
|
||||
video: '04.mp4'
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -102,7 +103,8 @@ export const mockDreams: Dream[] = [
|
||||
ai: {
|
||||
interpretation: 'Der Spiegelgarten repräsentiert Selbsterkenntnis und Rückblick auf die eigene Vergangenheit. Jeder Spiegel steht für ein Fragment der Erinnerung, das entfernte Lachen deutet auf positive Prägungen und emotionale Verankerung hin.',
|
||||
image: '05.png',
|
||||
audio: '05.mp3'
|
||||
audio: '05.mp3',
|
||||
video: '05.mp4'
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -208,7 +210,7 @@ export const mockDreams: Dream[] = [
|
||||
tags: ['Natur', 'Technologie', 'Innovation'],
|
||||
input: {
|
||||
inputType: 'image',
|
||||
img: 'digital_garden.jpg',
|
||||
img: 'in_11.png',
|
||||
imgAlt: 'Ein Garten aus leuchtenden Codezeilen und holografischen Pflanzen',
|
||||
description: 'Ich pflegte einen Garten, in dem Pflanzen aus Codezeilen und Algorithmen wuchsen. Jede Blüte entfaltete sich zu einer neuen Technologie.'
|
||||
} as ImageInput,
|
||||
|
@@ -11,6 +11,7 @@ import 'slick-carousel/slick/slick.css';
|
||||
import 'slick-carousel/slick/slick-theme.css';
|
||||
import {useEffect, useRef} from 'react';
|
||||
import * as d3 from 'd3';
|
||||
import DreamVR from '../components/DreamVR';
|
||||
|
||||
export default function DreamPage() {
|
||||
const {id} = useParams<{ id: string }>();
|
||||
@@ -530,6 +531,16 @@ export default function DreamPage() {
|
||||
</div>
|
||||
</div>)}
|
||||
|
||||
{/* VR Visualization */}
|
||||
{(dream.id === 10 || dream.input.inputType === 'chip') && (
|
||||
<div className="dreamy-card rounded-xl sm:rounded-2xl p-4 sm:p-6">
|
||||
<div className="flex items-center mb-4">
|
||||
<span className="font-medium dreamy-text">VR-Visualisierung</span>
|
||||
</div>
|
||||
<DreamVR dream={dream} height="600px"/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="dreamy-card rounded-xl sm:rounded-2xl p-4 sm:p-6">
|
||||
<h2 className="text-lg font-semibold mb-3 dreamy-text">Details</h2>
|
||||
<div className="space-y-3" style={{color: 'var(--text-muted)'}}>
|
||||
|
Reference in New Issue
Block a user