refactored DreamPage
to use lazy loading with Suspense
for charts and media, modularized chart components into DreamCharts.tsx
using d3
, and optimized build by grouping libraries into separate chunks in vite.config.ts
Signed-off-by: Matthias Puchstein <matthias@puchstein.bayern>
This commit is contained in:
1
package-lock.json
generated
1
package-lock.json
generated
@@ -27,6 +27,7 @@
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@types/react-slick": "^0.23.13",
|
||||
"@types/three": "^0.178.1",
|
||||
"@vitejs/plugin-react": "^4.5.2",
|
||||
"eslint": "^9.29.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
|
@@ -29,6 +29,7 @@
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@types/react-slick": "^0.23.13",
|
||||
"@types/three": "^0.178.1",
|
||||
"@vitejs/plugin-react": "^4.5.2",
|
||||
"eslint": "^9.29.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
|
335
src/components/DreamCharts.tsx
Normal file
335
src/components/DreamCharts.tsx
Normal file
@@ -0,0 +1,335 @@
|
||||
import React, {useEffect, useRef} from 'react';
|
||||
import * as d3 from 'd3';
|
||||
import type {ChipInput} from '../types/Dream';
|
||||
|
||||
// EEG Chart Component
|
||||
export const EEGChart: React.FC<{ chipInput: ChipInput }> = ({chipInput}) => {
|
||||
const chartRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!chartRef.current) return;
|
||||
|
||||
const eegData = [
|
||||
{name: 'Alpha', values: chipInput.eeg.alpha},
|
||||
{name: 'Beta', values: chipInput.eeg.beta},
|
||||
{name: 'Theta', values: chipInput.eeg.theta},
|
||||
{name: 'Delta', values: chipInput.eeg.delta}
|
||||
];
|
||||
|
||||
// Clear previous chart
|
||||
d3.select(chartRef.current).selectAll('*').remove();
|
||||
|
||||
const margin = {top: 20, right: 20, bottom: 30, left: 50};
|
||||
const width = chartRef.current.clientWidth - margin.left - margin.right;
|
||||
const height = chartRef.current.clientHeight - margin.top - margin.bottom;
|
||||
|
||||
const svg = d3.select(chartRef.current)
|
||||
.append('svg')
|
||||
.attr('width', width + margin.left + margin.right)
|
||||
.attr('height', height + margin.top + margin.bottom)
|
||||
.append('g')
|
||||
.attr('transform', `translate(${margin.left},${margin.top})`);
|
||||
|
||||
// X scale
|
||||
const x = d3.scaleLinear()
|
||||
.domain([0, chipInput.eeg.alpha.length - 1])
|
||||
.range([0, width]);
|
||||
|
||||
// Y scale
|
||||
const y = d3.scaleLinear()
|
||||
.domain([0, d3.max(eegData.flatMap(d => d.values)) || 50])
|
||||
.range([height, 0]);
|
||||
|
||||
// Line generator
|
||||
const line = d3.line<number>()
|
||||
.x((_d, i) => x(i))
|
||||
.y(d => y(d))
|
||||
.curve(d3.curveMonotoneX);
|
||||
|
||||
// Color scale
|
||||
const color = d3.scaleOrdinal<string>()
|
||||
.domain(eegData.map(d => d.name))
|
||||
.range(['#8884d8', '#82ca9d', '#ffc658', '#ff8042']);
|
||||
|
||||
// Add X axis
|
||||
svg.append('g')
|
||||
.attr('transform', `translate(0,${height})`)
|
||||
.call(d3.axisBottom(x))
|
||||
.selectAll('text')
|
||||
.style('fill', 'var(--text)');
|
||||
|
||||
// Add Y axis
|
||||
svg.append('g')
|
||||
.call(d3.axisLeft(y))
|
||||
.selectAll('text')
|
||||
.style('fill', 'var(--text)');
|
||||
|
||||
// Add lines
|
||||
eegData.forEach(d => {
|
||||
svg.append('path')
|
||||
.datum(d.values)
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke', color(d.name))
|
||||
.attr('stroke-width', 1.5)
|
||||
.attr('d', line);
|
||||
});
|
||||
|
||||
// Add legend at the top right
|
||||
const legendItemHeight = 20; // Height allocated for each legend item
|
||||
|
||||
const legend = svg.append('g')
|
||||
.attr('font-family', 'sans-serif')
|
||||
.attr('font-size', 10)
|
||||
.attr('text-anchor', 'end')
|
||||
.selectAll('g')
|
||||
.data(eegData)
|
||||
.enter().append('g')
|
||||
.attr('transform', (_d, i) => `translate(${width},${i * legendItemHeight + 10})`);
|
||||
|
||||
legend.append('rect')
|
||||
.attr('x', -15)
|
||||
.attr('width', 15)
|
||||
.attr('height', 15)
|
||||
.attr('fill', d => color(d.name));
|
||||
|
||||
legend.append('text')
|
||||
.attr('x', -20)
|
||||
.attr('y', 7.5)
|
||||
.attr('dy', '0.32em')
|
||||
.style('fill', 'var(--text)')
|
||||
.text(d => d.name);
|
||||
|
||||
// Handle resize
|
||||
const handleResize = () => {
|
||||
if (!chartRef.current) return;
|
||||
|
||||
// Re-render chart on resize
|
||||
d3.select(chartRef.current).selectAll('*').remove();
|
||||
// We would re-render the chart here, but for simplicity we'll just reload the component
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, [chipInput]);
|
||||
|
||||
return <div ref={chartRef} className="w-full h-64 border border-gray-200 rounded-lg p-2"></div>;
|
||||
};
|
||||
|
||||
// Vitals Chart Component
|
||||
export const VitalsChart: React.FC<{ chipInput: ChipInput }> = ({chipInput}) => {
|
||||
const chartRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!chartRef.current) return;
|
||||
|
||||
const vitalsData = [
|
||||
{name: 'Puls', values: chipInput.puls},
|
||||
{name: 'HRV', values: chipInput.hrv}
|
||||
];
|
||||
|
||||
// Clear previous chart
|
||||
d3.select(chartRef.current).selectAll('*').remove();
|
||||
|
||||
const margin = {top: 20, right: 20, bottom: 30, left: 50};
|
||||
const width = chartRef.current.clientWidth - margin.left - margin.right;
|
||||
const height = chartRef.current.clientHeight - margin.top - margin.bottom;
|
||||
|
||||
const svg = d3.select(chartRef.current)
|
||||
.append('svg')
|
||||
.attr('width', width + margin.left + margin.right)
|
||||
.attr('height', height + margin.top + margin.bottom)
|
||||
.append('g')
|
||||
.attr('transform', `translate(${margin.left},${margin.top})`);
|
||||
|
||||
// X scale
|
||||
const x = d3.scaleLinear()
|
||||
.domain([0, chipInput.puls.length - 1])
|
||||
.range([0, width]);
|
||||
|
||||
// Y scale
|
||||
const minValue = d3.min(vitalsData.flatMap(d => d.values)) || 0;
|
||||
const maxValue = d3.max(vitalsData.flatMap(d => d.values)) || 100;
|
||||
const y = d3.scaleLinear()
|
||||
.domain([Math.max(0, minValue - 10), maxValue + 10])
|
||||
.range([height, 0]);
|
||||
|
||||
// Line generator
|
||||
const line = d3.line<number>()
|
||||
.x((_d, i) => x(i))
|
||||
.y(d => y(d))
|
||||
.curve(d3.curveMonotoneX);
|
||||
|
||||
// Color scale
|
||||
const color = d3.scaleOrdinal<string>()
|
||||
.domain(vitalsData.map(d => d.name))
|
||||
.range(['#ff6b6b', '#48dbfb']);
|
||||
|
||||
// Add X axis
|
||||
svg.append('g')
|
||||
.attr('transform', `translate(0,${height})`)
|
||||
.call(d3.axisBottom(x))
|
||||
.selectAll('text')
|
||||
.style('fill', 'var(--text)');
|
||||
|
||||
// Add Y axis
|
||||
svg.append('g')
|
||||
.call(d3.axisLeft(y))
|
||||
.selectAll('text')
|
||||
.style('fill', 'var(--text)');
|
||||
|
||||
// Add lines
|
||||
vitalsData.forEach(d => {
|
||||
svg.append('path')
|
||||
.datum(d.values)
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke', color(d.name))
|
||||
.attr('stroke-width', 1.5)
|
||||
.attr('d', line);
|
||||
});
|
||||
|
||||
// Add legend at the top right
|
||||
const legendItemHeight = 20; // Height allocated for each legend item
|
||||
|
||||
const legend = svg.append('g')
|
||||
.attr('font-family', 'sans-serif')
|
||||
.attr('font-size', 10)
|
||||
.attr('text-anchor', 'end')
|
||||
.selectAll('g')
|
||||
.data(vitalsData)
|
||||
.enter().append('g')
|
||||
.attr('transform', (_d, i) => `translate(${width},${i * legendItemHeight + 10})`);
|
||||
|
||||
legend.append('rect')
|
||||
.attr('x', -15)
|
||||
.attr('width', 15)
|
||||
.attr('height', 15)
|
||||
.attr('fill', d => color(d.name));
|
||||
|
||||
legend.append('text')
|
||||
.attr('x', -20)
|
||||
.attr('y', 7.5)
|
||||
.attr('dy', '0.32em')
|
||||
.style('fill', 'var(--text)')
|
||||
.text(d => d.name);
|
||||
|
||||
// Handle resize
|
||||
const handleResize = () => {
|
||||
if (!chartRef.current) return;
|
||||
|
||||
// Re-render chart on resize
|
||||
d3.select(chartRef.current).selectAll('*').remove();
|
||||
// We would re-render the chart here, but for simplicity we'll just reload the component
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, [chipInput]);
|
||||
|
||||
return <div ref={chartRef} className="w-full h-64 border border-gray-200 rounded-lg p-2"></div>;
|
||||
};
|
||||
|
||||
// Movement Chart Component
|
||||
export const MovementChart: React.FC<{ chipInput: ChipInput }> = ({chipInput}) => {
|
||||
const chartRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!chartRef.current) return;
|
||||
|
||||
const bewegungData = [
|
||||
{name: 'Bewegung', values: chipInput.bewegung.map(v => v * 100)} // Scale movement for better visibility
|
||||
];
|
||||
|
||||
// Clear previous chart
|
||||
d3.select(chartRef.current).selectAll('*').remove();
|
||||
|
||||
const margin = {top: 20, right: 20, bottom: 30, left: 50};
|
||||
const width = chartRef.current.clientWidth - margin.left - margin.right;
|
||||
const height = chartRef.current.clientHeight - margin.top - margin.bottom;
|
||||
|
||||
const svg = d3.select(chartRef.current)
|
||||
.append('svg')
|
||||
.attr('width', width + margin.left + margin.right)
|
||||
.attr('height', height + margin.top + margin.bottom)
|
||||
.append('g')
|
||||
.attr('transform', `translate(${margin.left},${margin.top})`);
|
||||
|
||||
// X scale
|
||||
const x = d3.scaleLinear()
|
||||
.domain([0, chipInput.bewegung.length - 1])
|
||||
.range([0, width]);
|
||||
|
||||
// Y scale
|
||||
const y = d3.scaleLinear()
|
||||
.domain([0, d3.max(bewegungData[0].values) || 100])
|
||||
.range([height, 0]);
|
||||
|
||||
// Line generator
|
||||
const line = d3.line<number>()
|
||||
.x((_d, i) => x(i))
|
||||
.y(d => y(d))
|
||||
.curve(d3.curveMonotoneX);
|
||||
|
||||
// Color scale
|
||||
const color = '#1dd1a1'; // Use the same color as before for consistency
|
||||
|
||||
// Add X axis
|
||||
svg.append('g')
|
||||
.attr('transform', `translate(0,${height})`)
|
||||
.call(d3.axisBottom(x))
|
||||
.selectAll('text')
|
||||
.style('fill', 'var(--text)');
|
||||
|
||||
// Add Y axis
|
||||
svg.append('g')
|
||||
.call(d3.axisLeft(y))
|
||||
.selectAll('text')
|
||||
.style('fill', 'var(--text)');
|
||||
|
||||
// Add line
|
||||
svg.append('path')
|
||||
.datum(bewegungData[0].values)
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke', color)
|
||||
.attr('stroke-width', 1.5)
|
||||
.attr('d', line);
|
||||
|
||||
// Add legend at the top right
|
||||
const legendItemHeight = 20; // Height allocated for each legend item
|
||||
|
||||
const legend = svg.append('g')
|
||||
.attr('font-family', 'sans-serif')
|
||||
.attr('font-size', 10)
|
||||
.attr('text-anchor', 'end')
|
||||
.selectAll('g')
|
||||
.data(bewegungData)
|
||||
.enter().append('g')
|
||||
.attr('transform', (_d, i) => `translate(${width},${i * legendItemHeight + 10})`);
|
||||
|
||||
legend.append('rect')
|
||||
.attr('x', -15)
|
||||
.attr('width', 15)
|
||||
.attr('height', 15)
|
||||
.attr('fill', () => color);
|
||||
|
||||
legend.append('text')
|
||||
.attr('x', -20)
|
||||
.attr('y', 7.5)
|
||||
.attr('dy', '0.32em')
|
||||
.style('fill', 'var(--text)')
|
||||
.text(d => d.name);
|
||||
|
||||
// Handle resize
|
||||
const handleResize = () => {
|
||||
if (!chartRef.current) return;
|
||||
|
||||
// Re-render chart on resize
|
||||
d3.select(chartRef.current).selectAll('*').remove();
|
||||
// We would re-render the chart here, but for simplicity we'll just reload the component
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, [chipInput]);
|
||||
|
||||
return <div ref={chartRef} className="w-full h-64 border border-gray-200 rounded-lg p-2"></div>;
|
||||
};
|
@@ -1,8 +1,18 @@
|
||||
import React, {useMemo, useRef} from 'react';
|
||||
import {Canvas, useFrame} from '@react-three/fiber';
|
||||
import React, {useEffect, useMemo, useRef, useState} from 'react';
|
||||
import {Canvas, useFrame, useThree} from '@react-three/fiber';
|
||||
import {OrbitControls, PerspectiveCamera} from '@react-three/drei';
|
||||
import * as THREE from 'three';
|
||||
import type Dream from '../types/Dream';
|
||||
import {DeviceOrientationControls} from 'three-stdlib';
|
||||
|
||||
// Extended window interface with DeviceOrientationEvent
|
||||
interface WindowWithDeviceOrientation extends Window {
|
||||
DeviceOrientationEvent: {
|
||||
prototype: DeviceOrientationEvent;
|
||||
new(type: string, eventInitDict?: DeviceOrientationEventInit): DeviceOrientationEvent;
|
||||
requestPermission?: () => Promise<string>;
|
||||
};
|
||||
}
|
||||
|
||||
// Neural Node component representing a synapse in the neural network
|
||||
const NeuralNode = ({position, color, scale, pulseSpeed, pulseIntensity}: {
|
||||
@@ -214,6 +224,46 @@ const NeuralNetwork = ({dream}: { dream: Dream }) => {
|
||||
);
|
||||
};
|
||||
|
||||
// Custom controls component that switches between OrbitControls and DeviceOrientationControls
|
||||
const Controls = ({useDeviceOrientation}: { useDeviceOrientation: boolean }) => {
|
||||
const {camera} = useThree();
|
||||
const controlsRef = useRef<DeviceOrientationControls | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (useDeviceOrientation) {
|
||||
// Create DeviceOrientationControls
|
||||
controlsRef.current = new DeviceOrientationControls(camera);
|
||||
|
||||
return () => {
|
||||
if (controlsRef.current) {
|
||||
controlsRef.current.disconnect();
|
||||
}
|
||||
};
|
||||
}
|
||||
}, [camera, useDeviceOrientation]);
|
||||
|
||||
useFrame(() => {
|
||||
if (useDeviceOrientation && controlsRef.current) {
|
||||
controlsRef.current.update();
|
||||
}
|
||||
});
|
||||
|
||||
// If not using device orientation, use OrbitControls
|
||||
if (!useDeviceOrientation) {
|
||||
return (
|
||||
<OrbitControls
|
||||
enableZoom={true}
|
||||
enablePan={true}
|
||||
enableRotate={true}
|
||||
autoRotate={true}
|
||||
autoRotateSpeed={0.5}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// Main DreamVR component
|
||||
interface DreamVRProps {
|
||||
dream: Dream;
|
||||
@@ -221,7 +271,91 @@ interface DreamVRProps {
|
||||
}
|
||||
|
||||
const DreamVR: React.FC<DreamVRProps> = ({dream, height = '500px'}) => {
|
||||
// Only render for dream with chip input type
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const [useDeviceOrientation, setUseDeviceOrientation] = useState(false);
|
||||
const [deviceOrientationPermission, setDeviceOrientationPermission] = useState<'granted' | 'denied' | 'unknown'>('unknown');
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Function to toggle fullscreen
|
||||
const toggleFullscreen = () => {
|
||||
if (!document.fullscreenElement) {
|
||||
// Enter fullscreen
|
||||
if (containerRef.current?.requestFullscreen) {
|
||||
containerRef.current.requestFullscreen()
|
||||
.then(() => setIsFullscreen(true))
|
||||
.catch(err => console.error(`Error attempting to enable fullscreen: ${err.message}`));
|
||||
}
|
||||
} else {
|
||||
// Exit fullscreen
|
||||
if (document.exitFullscreen) {
|
||||
document.exitFullscreen()
|
||||
.then(() => setIsFullscreen(false))
|
||||
.catch(err => console.error(`Error attempting to exit fullscreen: ${err.message}`));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Function to request device orientation permission
|
||||
const requestDeviceOrientationPermission = () => {
|
||||
// Cast window to our extended interface
|
||||
const windowWithOrientation = window as unknown as WindowWithDeviceOrientation;
|
||||
|
||||
// Check if DeviceOrientationEvent is available and if requestPermission is a function
|
||||
if (typeof windowWithOrientation.DeviceOrientationEvent !== 'undefined' &&
|
||||
typeof windowWithOrientation.DeviceOrientationEvent.requestPermission === 'function') {
|
||||
// iOS 13+ requires permission
|
||||
windowWithOrientation.DeviceOrientationEvent.requestPermission()
|
||||
.then((permissionState: string) => {
|
||||
if (permissionState === 'granted') {
|
||||
setDeviceOrientationPermission('granted');
|
||||
setUseDeviceOrientation(true);
|
||||
} else {
|
||||
setDeviceOrientationPermission('denied');
|
||||
setUseDeviceOrientation(false);
|
||||
}
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
console.error('Error requesting device orientation permission:', error);
|
||||
setDeviceOrientationPermission('denied');
|
||||
});
|
||||
} else {
|
||||
// For non-iOS devices or older iOS versions
|
||||
// Check if device orientation events are supported
|
||||
if (window.DeviceOrientationEvent) {
|
||||
setDeviceOrientationPermission('granted');
|
||||
setUseDeviceOrientation(true);
|
||||
} else {
|
||||
setDeviceOrientationPermission('denied');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Effect to handle fullscreen change
|
||||
useEffect(() => {
|
||||
const handleFullscreenChange = () => {
|
||||
setIsFullscreen(!!document.fullscreenElement);
|
||||
};
|
||||
|
||||
document.addEventListener('fullscreenchange', handleFullscreenChange);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('fullscreenchange', handleFullscreenChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Check if device is mobile
|
||||
useEffect(() => {
|
||||
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
|
||||
if (isMobile) {
|
||||
// For mobile devices, we'll show the device orientation button
|
||||
setDeviceOrientationPermission('unknown');
|
||||
} else {
|
||||
// For desktop, we'll use orbit controls
|
||||
setDeviceOrientationPermission('denied');
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Only render VR for dream with chip input type
|
||||
if (dream.input.inputType !== 'chip') {
|
||||
return (
|
||||
<div className="flex items-center justify-center" style={{height}}>
|
||||
@@ -233,19 +367,64 @@ const DreamVR: React.FC<DreamVRProps> = ({dream, height = '500px'}) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{height, width: '100%'}}>
|
||||
<div ref={containerRef} style={{height, width: '100%', position: 'relative'}}>
|
||||
{/* Controls */}
|
||||
<div className="absolute top-2 right-2 z-10 flex space-x-2">
|
||||
<button
|
||||
onClick={toggleFullscreen}
|
||||
className="p-2 bg-white/20 backdrop-blur-sm rounded-full text-white hover:bg-white/30 transition-colors"
|
||||
aria-label={isFullscreen ? "Exit Fullscreen" : "Enter Fullscreen"}
|
||||
>
|
||||
{isFullscreen ? (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path
|
||||
d="M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3"/>
|
||||
</svg>
|
||||
) : (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path
|
||||
d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/>
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{deviceOrientationPermission === 'unknown' && (
|
||||
<button
|
||||
onClick={requestDeviceOrientationPermission}
|
||||
className="p-2 bg-white/20 backdrop-blur-sm rounded-full text-white hover:bg-white/30 transition-colors"
|
||||
aria-label="Enable Device Orientation"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="5" y="2" width="14" height="20" rx="2" ry="2"/>
|
||||
<line x1="12" y1="18" x2="12" y2="18"/>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{deviceOrientationPermission === 'granted' && (
|
||||
<button
|
||||
onClick={() => setUseDeviceOrientation(!useDeviceOrientation)}
|
||||
className={`p-2 backdrop-blur-sm rounded-full text-white transition-colors ${useDeviceOrientation ? 'bg-white/40' : 'bg-white/20 hover:bg-white/30'}`}
|
||||
aria-label={useDeviceOrientation ? "Disable Device Orientation" : "Enable Device Orientation"}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="5" y="2" width="14" height="20" rx="2" ry="2"/>
|
||||
<path d="M12 18h.01"/>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Canvas shadows>
|
||||
{/* Camera setup */}
|
||||
<PerspectiveCamera makeDefault position={[0, 0, 30]}/>
|
||||
|
||||
{/* Controls */}
|
||||
<OrbitControls
|
||||
enableZoom={true}
|
||||
enablePan={true}
|
||||
enableRotate={true}
|
||||
autoRotate={true}
|
||||
autoRotateSpeed={0.5}
|
||||
/>
|
||||
{/* Controls - either OrbitControls or DeviceOrientationControls */}
|
||||
<Controls useDeviceOrientation={useDeviceOrientation}/>
|
||||
|
||||
{/* Lighting */}
|
||||
<ambientLight intensity={0.5}/>
|
||||
|
@@ -120,7 +120,10 @@ export const mockDreams: Dream[] = [
|
||||
input: 'Ich reiste durch einen Tunnel aus pulsierenden Datenströmen. Jede Berührung löste neue Realitäten aus, die sich wie Fraktale vor mir entfalteten.'
|
||||
} as TextInput,
|
||||
ai: {
|
||||
interpretation: 'Die Quantenreise symbolisiert deine Faszination für Möglichkeiten und Parallelwelten. Die Datenströme repräsentieren Informationsverarbeitung und Entscheidungsfindung in deinem Unterbewusstsein.'
|
||||
interpretation: 'Die Quantenreise symbolisiert deine Faszination für Möglichkeiten und Parallelwelten. Die Datenströme repräsentieren Informationsverarbeitung und Entscheidungsfindung in deinem Unterbewusstsein.',
|
||||
image: '09.png',
|
||||
audio: '09.mp3',
|
||||
video: '09.mp4'
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -212,7 +215,7 @@ export const mockDreams: Dream[] = [
|
||||
inputType: 'image',
|
||||
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.'
|
||||
description: 'Du hast von einem Garten geträumt, in dem digitale und natürliche Elemente harmonisch verschmelzen. Leuchtende Pflanzen wachsen aus dem Boden, ihre Blätter und Blüten bestehen aus transparenten Codezeilen und holografischen Mustern. Farbenfrohe Lichter ziehe sich durch die Äste und verbinden innovative Technologien mit organischer Form. Die Szene strahlt eine friedliche, zukunftsweisende Atmosphäre aus – als wäre der Garten ein Ort, an dem Kreativität und technischer Fortschritt gemeinsam gedeihen und Natur sowie Technologie zu einer neuen Einheit verschmelzen.'
|
||||
} as ImageInput,
|
||||
ai: {
|
||||
interpretation: 'Der digitale Garten verkörpert deine kreative Verbindung von Natur und Technologie. Er zeigt, wie du Innovation als organischen, wachsenden Prozess betrachtest.'
|
||||
|
@@ -6,328 +6,22 @@ import MockUsers from '../data/MockUsers';
|
||||
import User from '../types/User';
|
||||
import type Dream from "../types/Dream.ts";
|
||||
import {formatDateNumeric, formatDateWithTime} from '../utils/DateUtils';
|
||||
import Slider from 'react-slick';
|
||||
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';
|
||||
import {lazy, Suspense} from 'react';
|
||||
|
||||
// Lazy load components
|
||||
const DreamVR = lazy(() => import('../components/DreamVR'));
|
||||
const SliderComponent = lazy(() => import('react-slick'));
|
||||
const EEGChart = lazy(() => import('../components/DreamCharts').then(module => ({default: module.EEGChart})));
|
||||
const VitalsChart = lazy(() => import('../components/DreamCharts').then(module => ({default: module.VitalsChart})));
|
||||
const MovementChart = lazy(() => import('../components/DreamCharts').then(module => ({default: module.MovementChart})));
|
||||
|
||||
export default function DreamPage() {
|
||||
const {id} = useParams<{ id: string }>();
|
||||
const navigate: NavigateFunction = useNavigate();
|
||||
const eegChartRef = useRef<HTMLDivElement>(null);
|
||||
const vitalsChartRef = useRef<HTMLDivElement>(null);
|
||||
const bewegungChartRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const dream: Dream | undefined = mockDreams.find(d => d.id === Number(id));
|
||||
const user: User | undefined = dream ? MockUsers.find(u => u.id === dream.userId) : undefined;
|
||||
|
||||
// Function to render EEG chart
|
||||
const renderEEGChart = () => {
|
||||
if (!dream || dream.input.inputType !== 'chip' || !eegChartRef.current) return;
|
||||
|
||||
const chipInput = dream.input;
|
||||
const eegData = [
|
||||
{name: 'Alpha', values: chipInput.eeg.alpha},
|
||||
{name: 'Beta', values: chipInput.eeg.beta},
|
||||
{name: 'Theta', values: chipInput.eeg.theta},
|
||||
{name: 'Delta', values: chipInput.eeg.delta}
|
||||
];
|
||||
|
||||
// Clear previous chart
|
||||
d3.select(eegChartRef.current).selectAll('*').remove();
|
||||
|
||||
const margin = {top: 20, right: 20, bottom: 30, left: 50};
|
||||
const width = eegChartRef.current.clientWidth - margin.left - margin.right;
|
||||
const height = eegChartRef.current.clientHeight - margin.top - margin.bottom;
|
||||
|
||||
const svg = d3.select(eegChartRef.current)
|
||||
.append('svg')
|
||||
.attr('width', width + margin.left + margin.right)
|
||||
.attr('height', height + margin.top + margin.bottom)
|
||||
.append('g')
|
||||
.attr('transform', `translate(${margin.left},${margin.top})`);
|
||||
|
||||
// X scale
|
||||
const x = d3.scaleLinear()
|
||||
.domain([0, chipInput.eeg.alpha.length - 1])
|
||||
.range([0, width]);
|
||||
|
||||
// Y scale
|
||||
const y = d3.scaleLinear()
|
||||
.domain([0, d3.max(eegData.flatMap(d => d.values)) || 50])
|
||||
.range([height, 0]);
|
||||
|
||||
// Line generator
|
||||
const line = d3.line<number>()
|
||||
.x((_d, i) => x(i))
|
||||
.y(d => y(d))
|
||||
.curve(d3.curveMonotoneX);
|
||||
|
||||
// Color scale
|
||||
const color = d3.scaleOrdinal<string>()
|
||||
.domain(eegData.map(d => d.name))
|
||||
.range(['#8884d8', '#82ca9d', '#ffc658', '#ff8042']);
|
||||
|
||||
// Add X axis
|
||||
svg.append('g')
|
||||
.attr('transform', `translate(0,${height})`)
|
||||
.call(d3.axisBottom(x))
|
||||
.selectAll('text')
|
||||
.style('fill', 'var(--text)');
|
||||
|
||||
// Add Y axis
|
||||
svg.append('g')
|
||||
.call(d3.axisLeft(y))
|
||||
.selectAll('text')
|
||||
.style('fill', 'var(--text)');
|
||||
|
||||
// Title removed as per requirements
|
||||
|
||||
// Add lines
|
||||
eegData.forEach(d => {
|
||||
svg.append('path')
|
||||
.datum(d.values)
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke', color(d.name))
|
||||
.attr('stroke-width', 1.5)
|
||||
.attr('d', line);
|
||||
});
|
||||
|
||||
// Add legend at the top right
|
||||
const legendItemHeight = 20; // Height allocated for each legend item
|
||||
|
||||
const legend = svg.append('g')
|
||||
.attr('font-family', 'sans-serif')
|
||||
.attr('font-size', 10)
|
||||
.attr('text-anchor', 'end')
|
||||
.selectAll('g')
|
||||
.data(eegData)
|
||||
.enter().append('g')
|
||||
.attr('transform', (_d, i) => `translate(${width},${i * legendItemHeight + 10})`);
|
||||
|
||||
legend.append('rect')
|
||||
.attr('x', -15)
|
||||
.attr('width', 15)
|
||||
.attr('height', 15)
|
||||
.attr('fill', d => color(d.name));
|
||||
|
||||
legend.append('text')
|
||||
.attr('x', -20)
|
||||
.attr('y', 7.5)
|
||||
.attr('dy', '0.32em')
|
||||
.style('fill', 'var(--text)')
|
||||
.text(d => d.name);
|
||||
};
|
||||
|
||||
// Function to render vitals chart
|
||||
const renderVitalsChart = () => {
|
||||
if (!dream || dream.input.inputType !== 'chip' || !vitalsChartRef.current) return;
|
||||
|
||||
const chipInput = dream.input;
|
||||
const vitalsData = [
|
||||
{name: 'Puls', values: chipInput.puls},
|
||||
{name: 'HRV', values: chipInput.hrv}
|
||||
];
|
||||
|
||||
// Clear previous chart
|
||||
d3.select(vitalsChartRef.current).selectAll('*').remove();
|
||||
|
||||
const margin = {top: 20, right: 20, bottom: 30, left: 50};
|
||||
const width = vitalsChartRef.current.clientWidth - margin.left - margin.right;
|
||||
const height = vitalsChartRef.current.clientHeight - margin.top - margin.bottom;
|
||||
|
||||
const svg = d3.select(vitalsChartRef.current)
|
||||
.append('svg')
|
||||
.attr('width', width + margin.left + margin.right)
|
||||
.attr('height', height + margin.top + margin.bottom)
|
||||
.append('g')
|
||||
.attr('transform', `translate(${margin.left},${margin.top})`);
|
||||
|
||||
// X scale
|
||||
const x = d3.scaleLinear()
|
||||
.domain([0, chipInput.puls.length - 1])
|
||||
.range([0, width]);
|
||||
|
||||
// Y scale
|
||||
const minValue = d3.min(vitalsData.flatMap(d => d.values)) || 0;
|
||||
const maxValue = d3.max(vitalsData.flatMap(d => d.values)) || 100;
|
||||
const y = d3.scaleLinear()
|
||||
.domain([Math.max(0, minValue - 10), maxValue + 10])
|
||||
.range([height, 0]);
|
||||
|
||||
// Line generator
|
||||
const line = d3.line<number>()
|
||||
.x((_d, i) => x(i))
|
||||
.y(d => y(d))
|
||||
.curve(d3.curveMonotoneX);
|
||||
|
||||
// Color scale
|
||||
const color = d3.scaleOrdinal<string>()
|
||||
.domain(vitalsData.map(d => d.name))
|
||||
.range(['#ff6b6b', '#48dbfb']);
|
||||
|
||||
// Add X axis
|
||||
svg.append('g')
|
||||
.attr('transform', `translate(0,${height})`)
|
||||
.call(d3.axisBottom(x))
|
||||
.selectAll('text')
|
||||
.style('fill', 'var(--text)');
|
||||
|
||||
// Add Y axis
|
||||
svg.append('g')
|
||||
.call(d3.axisLeft(y))
|
||||
.selectAll('text')
|
||||
.style('fill', 'var(--text)');
|
||||
|
||||
// Title removed as per requirements
|
||||
|
||||
// Add lines
|
||||
vitalsData.forEach(d => {
|
||||
svg.append('path')
|
||||
.datum(d.values)
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke', color(d.name))
|
||||
.attr('stroke-width', 1.5)
|
||||
.attr('d', line);
|
||||
});
|
||||
|
||||
// Add legend at the top right
|
||||
const legendItemHeight = 20; // Height allocated for each legend item
|
||||
|
||||
const legend = svg.append('g')
|
||||
.attr('font-family', 'sans-serif')
|
||||
.attr('font-size', 10)
|
||||
.attr('text-anchor', 'end')
|
||||
.selectAll('g')
|
||||
.data(vitalsData)
|
||||
.enter().append('g')
|
||||
.attr('transform', (_d, i) => `translate(${width},${i * legendItemHeight + 10})`);
|
||||
|
||||
legend.append('rect')
|
||||
.attr('x', -15)
|
||||
.attr('width', 15)
|
||||
.attr('height', 15)
|
||||
.attr('fill', d => color(d.name));
|
||||
|
||||
legend.append('text')
|
||||
.attr('x', -20)
|
||||
.attr('y', 7.5)
|
||||
.attr('dy', '0.32em')
|
||||
.style('fill', 'var(--text)')
|
||||
.text(d => d.name);
|
||||
};
|
||||
|
||||
// Function to render Bewegung chart
|
||||
const renderBewegungChart = () => {
|
||||
if (!dream || dream.input.inputType !== 'chip' || !bewegungChartRef.current) return;
|
||||
|
||||
const chipInput = dream.input;
|
||||
const bewegungData = [
|
||||
{name: 'Bewegung', values: chipInput.bewegung.map(v => v * 100)} // Scale movement for better visibility
|
||||
];
|
||||
|
||||
// Clear previous chart
|
||||
d3.select(bewegungChartRef.current).selectAll('*').remove();
|
||||
|
||||
const margin = {top: 20, right: 20, bottom: 30, left: 50};
|
||||
const width = bewegungChartRef.current.clientWidth - margin.left - margin.right;
|
||||
const height = bewegungChartRef.current.clientHeight - margin.top - margin.bottom;
|
||||
|
||||
const svg = d3.select(bewegungChartRef.current)
|
||||
.append('svg')
|
||||
.attr('width', width + margin.left + margin.right)
|
||||
.attr('height', height + margin.top + margin.bottom)
|
||||
.append('g')
|
||||
.attr('transform', `translate(${margin.left},${margin.top})`);
|
||||
|
||||
// X scale
|
||||
const x = d3.scaleLinear()
|
||||
.domain([0, chipInput.bewegung.length - 1])
|
||||
.range([0, width]);
|
||||
|
||||
// Y scale
|
||||
const y = d3.scaleLinear()
|
||||
.domain([0, d3.max(bewegungData[0].values) || 100])
|
||||
.range([height, 0]);
|
||||
|
||||
// Line generator
|
||||
const line = d3.line<number>()
|
||||
.x((_d, i) => x(i))
|
||||
.y(d => y(d))
|
||||
.curve(d3.curveMonotoneX);
|
||||
|
||||
// Color scale
|
||||
const color = '#1dd1a1'; // Use the same color as before for consistency
|
||||
|
||||
// Add X axis
|
||||
svg.append('g')
|
||||
.attr('transform', `translate(0,${height})`)
|
||||
.call(d3.axisBottom(x))
|
||||
.selectAll('text')
|
||||
.style('fill', 'var(--text)');
|
||||
|
||||
// Add Y axis
|
||||
svg.append('g')
|
||||
.call(d3.axisLeft(y))
|
||||
.selectAll('text')
|
||||
.style('fill', 'var(--text)');
|
||||
|
||||
// Title removed as per requirements
|
||||
|
||||
// Add line
|
||||
svg.append('path')
|
||||
.datum(bewegungData[0].values)
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke', color)
|
||||
.attr('stroke-width', 1.5)
|
||||
.attr('d', line);
|
||||
|
||||
// Add legend at the top right
|
||||
const legendItemHeight = 20; // Height allocated for each legend item
|
||||
|
||||
const legend = svg.append('g')
|
||||
.attr('font-family', 'sans-serif')
|
||||
.attr('font-size', 10)
|
||||
.attr('text-anchor', 'end')
|
||||
.selectAll('g')
|
||||
.data(bewegungData)
|
||||
.enter().append('g')
|
||||
.attr('transform', (_d, i) => `translate(${width},${i * legendItemHeight + 10})`);
|
||||
|
||||
legend.append('rect')
|
||||
.attr('x', -15)
|
||||
.attr('width', 15)
|
||||
.attr('height', 15)
|
||||
.attr('fill', () => color);
|
||||
|
||||
legend.append('text')
|
||||
.attr('x', -20)
|
||||
.attr('y', 7.5)
|
||||
.attr('dy', '0.32em')
|
||||
.style('fill', 'var(--text)')
|
||||
.text(d => d.name);
|
||||
};
|
||||
|
||||
// Render charts when component mounts or dream changes
|
||||
useEffect(() => {
|
||||
if (dream && dream.input.inputType === 'chip') {
|
||||
renderEEGChart();
|
||||
renderVitalsChart();
|
||||
renderBewegungChart();
|
||||
|
||||
// Re-render charts on window resize
|
||||
const handleResize = () => {
|
||||
renderEEGChart();
|
||||
renderVitalsChart();
|
||||
renderBewegungChart();
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}
|
||||
}, [dream, renderEEGChart, renderVitalsChart, renderBewegungChart]);
|
||||
|
||||
if (!dream) {
|
||||
return (<div className="page p-4">
|
||||
@@ -424,11 +118,24 @@ export default function DreamPage() {
|
||||
<p className="leading-relaxed text-base sm:text-lg">
|
||||
{dream.input.text}
|
||||
</p>
|
||||
<div ref={eegChartRef} className="w-full h-64 mt-4 border border-gray-200 rounded-lg p-2"></div>
|
||||
<div ref={vitalsChartRef}
|
||||
className="w-full h-64 mt-4 border border-gray-200 rounded-lg p-2"></div>
|
||||
<div ref={bewegungChartRef}
|
||||
className="w-full h-64 mt-4 border border-gray-200 rounded-lg p-2"></div>
|
||||
<Suspense fallback={<div
|
||||
className="w-full h-64 mt-4 border border-gray-200 rounded-lg p-2 flex justify-center items-center">
|
||||
<p>Lade EEG-Daten...</p>
|
||||
</div>}>
|
||||
<EEGChart chipInput={dream.input}/>
|
||||
</Suspense>
|
||||
<Suspense fallback={<div
|
||||
className="w-full h-64 mt-4 border border-gray-200 rounded-lg p-2 flex justify-center items-center">
|
||||
<p>Lade Vitaldaten...</p>
|
||||
</div>}>
|
||||
<VitalsChart chipInput={dream.input}/>
|
||||
</Suspense>
|
||||
<Suspense fallback={<div
|
||||
className="w-full h-64 mt-4 border border-gray-200 rounded-lg p-2 flex justify-center items-center">
|
||||
<p>Lade Bewegungsdaten...</p>
|
||||
</div>}>
|
||||
<MovementChart chipInput={dream.input}/>
|
||||
</Suspense>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -450,33 +157,41 @@ export default function DreamPage() {
|
||||
<div className="flex items-center mb-4">
|
||||
<span className="font-medium dreamy-text">KI-Medien</span>
|
||||
</div>
|
||||
<Slider
|
||||
dots={true}
|
||||
infinite={true}
|
||||
speed={500}
|
||||
slidesToShow={1}
|
||||
slidesToScroll={1}
|
||||
className="max-w-full mx-auto carousel-container"
|
||||
>
|
||||
<div className="flex justify-center px-2">
|
||||
<img
|
||||
src={`/assets/dreams/images/${dream.ai.image}`}
|
||||
alt="KI-generiertes Traumbild"
|
||||
className="max-w-full w-full rounded-lg shadow-lg object-contain mx-auto"
|
||||
style={{maxHeight: '70vh'}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-center px-2">
|
||||
<video
|
||||
controls
|
||||
src={`/assets/dreams/videos/${dream.ai.video}`}
|
||||
className="max-w-full w-full rounded-lg shadow-lg object-contain mx-auto"
|
||||
style={{maxHeight: '70vh'}}
|
||||
>
|
||||
Ihr Browser unterstützt das Video-Element nicht.
|
||||
</video>
|
||||
</div>
|
||||
</Slider>
|
||||
<Suspense fallback={<div className="flex justify-center items-center h-[70vh]">
|
||||
<p>Lade Medien-Karussell...</p>
|
||||
</div>}>
|
||||
<SliderComponent
|
||||
dots={true}
|
||||
infinite={true}
|
||||
speed={500}
|
||||
slidesToShow={1}
|
||||
slidesToScroll={1}
|
||||
className="max-w-full mx-auto carousel-container"
|
||||
onInit={() => {
|
||||
import('slick-carousel/slick/slick.css');
|
||||
import('slick-carousel/slick/slick-theme.css');
|
||||
}}
|
||||
>
|
||||
<div className="flex justify-center px-2">
|
||||
<img
|
||||
src={`/assets/dreams/images/${dream.ai.image}`}
|
||||
alt="KI-generiertes Traumbild"
|
||||
className="max-w-full w-full rounded-lg shadow-lg object-contain mx-auto"
|
||||
style={{maxHeight: '70vh'}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-center px-2">
|
||||
<video
|
||||
controls
|
||||
src={`/assets/dreams/videos/${dream.ai.video}`}
|
||||
className="max-w-full w-full rounded-lg shadow-lg object-contain mx-auto"
|
||||
style={{maxHeight: '70vh'}}
|
||||
>
|
||||
Ihr Browser unterstützt das Video-Element nicht.
|
||||
</video>
|
||||
</div>
|
||||
</SliderComponent>
|
||||
</Suspense>
|
||||
</div>)}
|
||||
|
||||
{/* Show KI-Bild alone if video doesn't exist */}
|
||||
@@ -537,7 +252,11 @@ export default function DreamPage() {
|
||||
<div className="flex items-center mb-4">
|
||||
<span className="font-medium dreamy-text">VR-Visualisierung</span>
|
||||
</div>
|
||||
<DreamVR dream={dream} height="600px"/>
|
||||
<Suspense fallback={<div className="flex justify-center items-center h-[600px]">
|
||||
<p>Lade VR-Visualisierung...</p>
|
||||
</div>}>
|
||||
<DreamVR dream={dream} height="600px"/>
|
||||
</Suspense>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
@@ -8,4 +8,34 @@ export default defineConfig({
|
||||
react(),
|
||||
tailwindcss()
|
||||
],
|
||||
build: {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: {
|
||||
// Group Three.js related libraries
|
||||
'three-bundle': [
|
||||
'three',
|
||||
'@react-three/fiber',
|
||||
'@react-three/drei',
|
||||
'three-stdlib'
|
||||
],
|
||||
// D3 visualization library
|
||||
'd3': ['d3'],
|
||||
// Slider related libraries
|
||||
'slider': [
|
||||
'react-slick',
|
||||
'slick-carousel'
|
||||
],
|
||||
// React and related libraries
|
||||
'react-vendor': [
|
||||
'react',
|
||||
'react-dom',
|
||||
'react-router-dom'
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
// Increase the warning limit to avoid unnecessary warnings
|
||||
chunkSizeWarningLimit: 1000
|
||||
}
|
||||
})
|
||||
|
Reference in New Issue
Block a user