Spaces:
Sleeping
Sleeping
| import React, { useRef, useEffect, useState, useCallback } from 'react' | |
| import { AudioEngine } from '../engine/AudioEngine' | |
| import { | |
| ParticleSystemParams, | |
| ParticleSphere, | |
| DEFAULT_FREQUENCY_RANGES, | |
| DEFAULT_SPHERE_PARAMS, | |
| FogParams | |
| } from '../types/visualization' | |
| import { NoiseGenerator, BeatManager, generateNewNoiseScale } from '../utils/noise' | |
| import configLoader from '../utils/configLoader' | |
| interface WorldTreeVisualizerProps { | |
| audioEngine: AudioEngine | null | |
| } | |
| // ENTRONAUT INTEGRATION: Symbolic Emergence Field Analysis | |
| interface EntronautState { | |
| sefaScore: Float32Array | |
| emergenceField: Float32Array | |
| adaptiveParams: { | |
| damping: Float32Array | |
| diffusion: Float32Array | |
| coupling: Float32Array | |
| } | |
| informationMetrics: { | |
| complexity: number | |
| emergence: number | |
| coherence: number | |
| } | |
| } | |
| // Advanced 3D Particle System Visualizer - Enhanced with Entronaut SEFA | |
| export const WorldTreeVisualizer: React.FC<WorldTreeVisualizerProps> = ({ audioEngine }) => { | |
| const canvasRef = useRef<HTMLCanvasElement>(null) | |
| const animationRef = useRef<number>() | |
| const lastTimeRef = useRef<number>(0) | |
| // Three.js core objects | |
| const sceneRef = useRef<any>() | |
| const rendererRef = useRef<any>() | |
| const cameraRef = useRef<any>() | |
| const controlsRef = useRef<any>() | |
| // Advanced visualization state | |
| const spheresRef = useRef<ParticleSphere[]>([]) | |
| const noiseGeneratorRef = useRef<NoiseGenerator>() | |
| const beatManagerRef = useRef<BeatManager>() | |
| const tendrilSystemRef = useRef<any>() | |
| const dysonSphereRef = useRef<any>() | |
| // ENTRONAUT: Symbolic emergence tracking | |
| const entronautStateRef = useRef<EntronautState>() | |
| // State for UI controls | |
| const [isVisualizationReady, setIsVisualizationReady] = useState(false) | |
| const [activeSpheresCount, setActiveSpheresCount] = useState(5) // All spheres active by default | |
| const [cymateGeometry, setCymateGeometry] = useState(true) | |
| const [entronautEnabled, setEntronautEnabled] = useState(true) | |
| const [adaptiveCoupling, setAdaptiveCoupling] = useState(true) | |
| const [performanceMode, setPerformanceMode] = useState(false) | |
| const [controlsCollapsed, setControlsCollapsed] = useState(true) | |
| const [cameraControlsEnabled, setCameraControlsEnabled] = useState(true) | |
| const [autoRotate, setAutoRotate] = useState(false) | |
| const [aboutCollapsed, setAboutCollapsed] = useState(true) | |
| const [tendrilsEnabled, setTendrilsEnabled] = useState(true) | |
| const [tendrilDensity, setTendrilDensity] = useState(0.3) | |
| const [dysonSphereEnabled, setDysonSphereEnabled] = useState(true) | |
| const [vineGrowthRate, setVineGrowthRate] = useState(0.02) | |
| const [vineComplexity, setVineComplexity] = useState(24) | |
| const [showStartupInfo, setShowStartupInfo] = useState(true) | |
| const [fogParams, setFogParams] = useState<FogParams>({ | |
| enabled: true, | |
| color: '#000000', | |
| near: 2.7, | |
| far: 3.7 | |
| }) | |
| // Store state (currently minimal, expanded as needed) | |
| // const { playback } = useNexusStore() | |
| // Simplified performance monitoring - no complex optimizer | |
| const [performanceStats, setPerformanceStats] = useState({ | |
| fps: 60, | |
| particleCount: 0, | |
| lastFpsUpdate: 0, | |
| frameCount: 0 | |
| }) | |
| // Hidden Auto-Refresh System | |
| const autoRefreshRef = useRef({ | |
| lastRefresh: 0, | |
| refreshInterval: 120000, // 2 minutes base interval | |
| performanceDegradationCount: 0, | |
| particleHealthChecks: 0, | |
| emergencyResetCount: 0, | |
| isRefreshing: false | |
| }) | |
| // Monitor particle system health | |
| const checkParticleHealth = useCallback(() => { | |
| if (!spheresRef.current) return true | |
| let healthyParticles = 0 | |
| let totalParticles = 0 | |
| spheresRef.current.forEach(sphere => { | |
| if (!sphere.params.enabled) return | |
| const { positions, velocities } = sphere | |
| const particleCount = sphere.params.particleCount | |
| totalParticles += particleCount | |
| for (let i = 0; i < particleCount; i++) { | |
| const i3 = i * 3 | |
| const x = positions[i3] | |
| const y = positions[i3 + 1] | |
| const z = positions[i3 + 2] | |
| const vx = velocities[i3] | |
| const vy = velocities[i3 + 1] | |
| const vz = velocities[i3 + 2] | |
| // Check if particle is healthy (finite values, reasonable position/velocity) | |
| if (isFinite(x) && isFinite(y) && isFinite(z) && | |
| isFinite(vx) && isFinite(vy) && isFinite(vz)) { | |
| const dist = Math.sqrt(x*x + y*y + z*z) | |
| const vel = Math.sqrt(vx*vx + vy*vy + vz*vz) | |
| if (dist < 10 && vel < 1) { // Reasonable bounds | |
| healthyParticles++ | |
| } | |
| } | |
| } | |
| }) | |
| const healthRatio = totalParticles > 0 ? healthyParticles / totalParticles : 1 | |
| return healthRatio > 0.85 // 85% healthy particles threshold | |
| }, []) | |
| // Perform hidden refresh of particle systems | |
| const performHiddenRefresh = useCallback(async (force = false) => { | |
| if (autoRefreshRef.current.isRefreshing && !force) return | |
| autoRefreshRef.current.isRefreshing = true | |
| console.log('🔄 Performing hidden system refresh...') | |
| try { | |
| // Gradual refresh to avoid frame drops | |
| for (let sphereIndex = 0; sphereIndex < spheresRef.current.length; sphereIndex++) { | |
| const sphere = spheresRef.current[sphereIndex] | |
| if (!sphere.params.enabled) continue | |
| // Refresh 20% of particles at a time over 5 frames | |
| const particleCount = sphere.params.particleCount | |
| const batchSize = Math.floor(particleCount * 0.2) | |
| for (let batch = 0; batch < 5; batch++) { | |
| const startIdx = batch * batchSize | |
| const endIdx = Math.min(startIdx + batchSize, particleCount) | |
| // Reset particles in this batch | |
| for (let i = startIdx; i < endIdx; i++) { | |
| const i3 = i * 3 | |
| const radius = sphere.params.sphereRadius * sphere.params.innerSphereRadius | |
| const theta = Math.random() * Math.PI * 2 | |
| const phi = Math.acos(2 * Math.random() - 1) | |
| const r = Math.cbrt(Math.random()) * radius | |
| sphere.positions[i3] = r * Math.sin(phi) * Math.cos(theta) | |
| sphere.positions[i3 + 1] = r * Math.sin(phi) * Math.sin(theta) | |
| sphere.positions[i3 + 2] = r * Math.cos(phi) | |
| sphere.velocities[i3] = 0 | |
| sphere.velocities[i3 + 1] = 0 | |
| sphere.velocities[i3 + 2] = 0 | |
| sphere.lifetimes[i] = Math.random() * sphere.params.particleLifetime | |
| sphere.beatEffects[i] = 0 | |
| } | |
| // Wait one frame between batches to maintain smooth animation | |
| if (batch < 4) { | |
| await new Promise(resolve => requestAnimationFrame(() => resolve(undefined))) | |
| } | |
| } | |
| // Note: Geometry updates are handled by the main animation loop | |
| } | |
| // Reset counters | |
| autoRefreshRef.current.performanceDegradationCount = 0 | |
| autoRefreshRef.current.emergencyResetCount = 0 | |
| autoRefreshRef.current.lastRefresh = Date.now() | |
| console.log('✅ Hidden refresh completed successfully') | |
| } catch (error) { | |
| console.warn('⚠️ Hidden refresh error:', error) | |
| } finally { | |
| autoRefreshRef.current.isRefreshing = false | |
| } | |
| }, []) | |
| // ENTRONAUT: Initialize emergence field analysis - SIMPLIFIED for performance | |
| const initializeEntronaut = useCallback(() => { | |
| // Simplified - basic structure for UI display only | |
| console.log('🧠 Entronaut SEFA system simplified for performance') | |
| entronautStateRef.current = { | |
| sefaScore: new Float32Array(0), | |
| emergenceField: new Float32Array(0), | |
| adaptiveParams: { | |
| damping: new Float32Array(0), | |
| diffusion: new Float32Array(0), | |
| coupling: new Float32Array(0) | |
| }, | |
| informationMetrics: { | |
| complexity: 0, | |
| emergence: 0, | |
| coherence: 0 | |
| } | |
| } | |
| }, []) | |
| // ENTRONAUT: Analyze audio - SIMPLIFIED | |
| const analyzeSymbolicEmergence = useCallback((audioData: any, _currentTime: number) => { | |
| // Simplified - only basic metrics without complex calculations | |
| if (!entronautStateRef.current || !entronautEnabled) return | |
| // Just update basic metrics without heavy computation | |
| const { informationMetrics } = entronautStateRef.current | |
| informationMetrics.complexity = audioData.overallAmplitude || 0 | |
| informationMetrics.emergence = audioData.beatDetected ? 1.0 : 0.1 | |
| informationMetrics.coherence = (audioData.deepEnergy + audioData.midEnergy + audioData.highEnergy) / 3.0 | |
| }, [entronautEnabled]) | |
| // ENTRONAUT: Apply adaptive coupling - SIMPLIFIED | |
| const applyEntronautCoupling = useCallback((_sphere: ParticleSphere, _particleIndex: number) => { | |
| // Return simple defaults - no complex field calculations | |
| return { damping: 0.95, diffusion: 0.008, coupling: 0.02 } | |
| }, []) | |
| // Initialize Three.js scene and particle systems | |
| const initializeVisualization = useCallback(async () => { | |
| if (!canvasRef.current) return | |
| console.log('🎨 Initializing advanced particle visualization with Entronaut SEFA...') | |
| try { | |
| // Dynamically import Three.js | |
| const THREE = await import('three') | |
| // Create scene | |
| const scene = new THREE.Scene() | |
| scene.background = new THREE.Color(0x000000) | |
| sceneRef.current = scene | |
| // Create camera | |
| const camera = new THREE.PerspectiveCamera( | |
| 75, | |
| window.innerWidth / window.innerHeight, | |
| 0.1, | |
| 1000 | |
| ) | |
| camera.position.set(0, 0, 2.5) | |
| cameraRef.current = camera | |
| // Create renderer | |
| const renderer = new THREE.WebGLRenderer({ | |
| canvas: canvasRef.current!, | |
| antialias: true | |
| }) | |
| renderer.setSize(window.innerWidth, window.innerHeight) | |
| rendererRef.current = renderer | |
| // Initialize camera controls | |
| const { OrbitControls } = await import('three/examples/jsm/controls/OrbitControls.js') | |
| const controls = new OrbitControls(camera, renderer.domElement) | |
| // Configure controls | |
| controls.enableDamping = true | |
| controls.dampingFactor = 0.05 | |
| controls.enableZoom = true | |
| controls.enablePan = true | |
| controls.enableRotate = true | |
| // Set limits | |
| controls.maxDistance = 50 | |
| controls.minDistance = 0.1 | |
| controls.maxPolarAngle = Math.PI // Allow full rotation | |
| // Auto-rotate settings | |
| controls.autoRotate = autoRotate | |
| controls.autoRotateSpeed = 0.5 | |
| // Prevent controls from interfering with UI elements | |
| controls.addEventListener('change', () => { | |
| // Only update if camera controls are enabled | |
| if (!cameraControlsEnabled) { | |
| controls.enabled = false | |
| } | |
| }) | |
| controlsRef.current = controls | |
| // Initialize noise generator and beat manager | |
| noiseGeneratorRef.current = new NoiseGenerator() | |
| beatManagerRef.current = new BeatManager() | |
| // Initialize Entronaut | |
| initializeEntronaut() | |
| // Create particle spheres | |
| await createParticleSpheres(THREE, scene) | |
| // Create dynamic tendril system | |
| if (tendrilsEnabled) { | |
| await createTendrilSystem(THREE, scene) | |
| } | |
| // Create Dyson sphere/growing vines system | |
| if (dysonSphereEnabled) { | |
| await createDysonSphere(THREE, scene) | |
| } | |
| // Setup fog | |
| updateFog(THREE) | |
| setIsVisualizationReady(true) | |
| console.log('✅ Advanced particle visualization with Entronaut initialized') | |
| } catch (error) { | |
| console.error('❌ Failed to initialize visualization:', error) | |
| } | |
| }, [initializeEntronaut]) | |
| // Create multiple particle sphere systems | |
| const createParticleSpheres = useCallback(async (THREE: any, scene: any) => { | |
| const spheres: ParticleSphere[] = [] | |
| // Load organic color schemes from config | |
| const config = configLoader.getConfig() | |
| const configColorSchemes = (config as any)?.colors?.sphereColorSchemes | |
| // Enhanced organic color schemes for each sphere | |
| const colorSchemes = configColorSchemes || [ | |
| { start: '#2d5016', end: '#7d4f39' }, // Deep forest to iron oxide (sub-bass) | |
| { start: '#8b5a2b', end: '#4a6741' }, // Earth brown to vine green (bass) | |
| { start: '#cd853f', end: '#556b2f' }, // Sandy brown to moss green (mid) | |
| { start: '#daa520', end: '#2e8b57' }, // Gold leaf to sea green (high-mid) | |
| { start: '#b8860b', end: '#20b2aa' } // Burnished gold to teal depth (high) | |
| ] | |
| for (let i = 0; i < 5; i++) { | |
| const sphereParams: ParticleSystemParams = { | |
| ...DEFAULT_SPHERE_PARAMS, | |
| enabled: true, // All spheres enabled by default | |
| ...DEFAULT_FREQUENCY_RANGES[i] || DEFAULT_FREQUENCY_RANGES[0], | |
| colorStart: colorSchemes[i].start, | |
| colorEnd: colorSchemes[i].end, | |
| // Enhanced settings for organic, rich visuals | |
| particleCount: 15000, // Increased for more density | |
| turbulenceStrength: 0.006, // Reduced for more organic movement | |
| beatStrength: 0.015, // Gentler beat response for organic feel | |
| noiseScale: 2.5 + i * 0.3, // Smoother noise variation | |
| sphereRadius: 1.0 + i * 0.06, // More subtle spatial variation | |
| rotationSpeedMax: 0.04 + i * 0.006, // Slower, more organic rotation | |
| particleLifetime: 12.0 + i * 2.0 // Longer lifetimes for stability | |
| } | |
| const sphere = await createParticleSphere(THREE, scene, i, sphereParams) | |
| spheres.push(sphere) | |
| } | |
| spheresRef.current = spheres | |
| console.log(`🔮 Created ${spheres.length} particle sphere systems`) | |
| }, []) | |
| // Create dynamic tendril system that connects particles | |
| const createTendrilSystem = useCallback(async (THREE: any, scene: any) => { | |
| if (!tendrilsEnabled) return | |
| console.log('🌿 Creating dynamic tendril system...') | |
| // Create tendril network between particles | |
| const maxTendrils = 2000 // Maximum number of tendrils | |
| const maxTendrilLength = 0.8 // Maximum distance for tendril connections | |
| const tendrilSegments = 8 // Segments per tendril for smooth curves | |
| // Create tendril geometry | |
| const tendrilGeometry = new THREE.BufferGeometry() | |
| const tendrilPositions = new Float32Array(maxTendrils * tendrilSegments * 6) // 2 points per segment | |
| const tendrilColors = new Float32Array(maxTendrils * tendrilSegments * 6) // RGB for each point | |
| const tendrilOpacities = new Float32Array(maxTendrils * tendrilSegments * 2) // Alpha for each point | |
| // Initialize with empty data | |
| tendrilPositions.fill(0) | |
| tendrilColors.fill(0.3) // Dim gold base | |
| tendrilOpacities.fill(0) | |
| tendrilGeometry.setAttribute('position', new THREE.BufferAttribute(tendrilPositions, 3)) | |
| tendrilGeometry.setAttribute('color', new THREE.BufferAttribute(tendrilColors, 3)) | |
| tendrilGeometry.setAttribute('alpha', new THREE.BufferAttribute(tendrilOpacities, 1)) | |
| // Create custom shader material for tendrils | |
| const tendrilMaterial = new THREE.ShaderMaterial({ | |
| uniforms: { | |
| time: { value: 0.0 }, | |
| sefaField: { value: 0.0 }, | |
| audioEnergy: { value: 0.0 } | |
| }, | |
| vertexShader: ` | |
| attribute float alpha; | |
| varying float vAlpha; | |
| varying vec3 vColor; | |
| uniform float time; | |
| uniform float sefaField; | |
| uniform float audioEnergy; | |
| void main() { | |
| vAlpha = alpha * (0.3 + 0.7 * sin(time * 2.0 + position.x * 10.0)); | |
| vColor = color * (0.5 + 0.5 * audioEnergy); | |
| vec3 pos = position; | |
| // Add SEFA-driven undulation | |
| pos += 0.02 * sefaField * sin(time * 3.0 + position.y * 15.0) * normal; | |
| gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0); | |
| } | |
| `, | |
| fragmentShader: ` | |
| varying float vAlpha; | |
| varying vec3 vColor; | |
| void main() { | |
| // Create glowing tendril effect | |
| float glow = 1.0 - length(gl_PointCoord - vec2(0.5)); | |
| glow = pow(glow, 2.0); | |
| gl_FragColor = vec4(vColor, vAlpha * glow); | |
| } | |
| `, | |
| transparent: true, | |
| blending: THREE.AdditiveBlending, | |
| side: THREE.DoubleSide | |
| }) | |
| // Create tendril line system | |
| const tendrilLines = new THREE.LineSegments(tendrilGeometry, tendrilMaterial) | |
| tendrilLines.userData = { type: 'tendrils' } | |
| scene.add(tendrilLines) | |
| tendrilSystemRef.current = { | |
| geometry: tendrilGeometry, | |
| material: tendrilMaterial, | |
| lines: tendrilLines, | |
| positions: tendrilPositions, | |
| colors: tendrilColors, | |
| opacities: tendrilOpacities, | |
| maxTendrils, | |
| maxTendrilLength, | |
| tendrilSegments, | |
| lastUpdate: 0 | |
| } | |
| console.log('✅ Dynamic tendril system created') | |
| }, [tendrilsEnabled]) | |
| // Create Dyson sphere/growing vines system | |
| const createDysonSphere = useCallback(async (THREE: any, scene: any) => { | |
| if (!dysonSphereEnabled) return | |
| console.log('🌿 Creating Dyson sphere growing vines system...') | |
| const config = configLoader.getConfig() | |
| const dysonConfig = (config as any)?.dysonSphere | |
| const colors = (config as any)?.colors?.dysonVines | |
| if (!dysonConfig || !colors) { | |
| console.warn('Dyson sphere configuration missing') | |
| return | |
| } | |
| // Initialize vine system | |
| const maxVines = vineComplexity | |
| const maxSegments = dysonConfig.vines.maxSegmentsPerVine | |
| const sphereRadius = dysonConfig.vines.sphereRadius | |
| // Create vine geometry | |
| const vineGeometry = new THREE.BufferGeometry() | |
| const vinePositions = new Float32Array(maxVines * maxSegments * 6) // Line segments | |
| const vineColors = new Float32Array(maxVines * maxSegments * 6) | |
| const vineOpacities = new Float32Array(maxVines * maxSegments * 2) | |
| // Initialize with empty data | |
| vinePositions.fill(0) | |
| vineColors.fill(0) | |
| vineOpacities.fill(0) | |
| vineGeometry.setAttribute('position', new THREE.BufferAttribute(vinePositions, 3)) | |
| vineGeometry.setAttribute('color', new THREE.BufferAttribute(vineColors, 3)) | |
| vineGeometry.setAttribute('alpha', new THREE.BufferAttribute(vineOpacities, 1)) | |
| // Create node geometry for connection points | |
| const nodeGeometry = new THREE.SphereGeometry(dysonConfig.nodes.nodeSize, 8, 6) | |
| const nodeMaterial = new THREE.MeshBasicMaterial({ | |
| color: new THREE.Color(colors.nodeColor), | |
| transparent: true, | |
| opacity: 0.8 | |
| }) | |
| // Create vine shader material | |
| const vineMaterial = new THREE.ShaderMaterial({ | |
| uniforms: { | |
| time: { value: 0.0 }, | |
| growthProgress: { value: 0.0 }, | |
| audioEnergy: { value: 0.0 }, | |
| emergenceField: { value: 0.0 }, | |
| vineGlow: { value: dysonConfig.visual.vineGlow }, | |
| primaryColor: { value: new THREE.Color(colors.primaryVine) }, | |
| secondaryColor: { value: new THREE.Color(colors.secondaryVine) }, | |
| highlightColor: { value: new THREE.Color(colors.vineHighlight) } | |
| }, | |
| vertexShader: ` | |
| attribute float alpha; | |
| varying float vAlpha; | |
| varying vec3 vColor; | |
| varying vec3 vPosition; | |
| uniform float time; | |
| uniform float growthProgress; | |
| uniform float audioEnergy; | |
| uniform vec3 primaryColor; | |
| uniform vec3 secondaryColor; | |
| uniform vec3 highlightColor; | |
| void main() { | |
| vAlpha = alpha * (0.4 + 0.6 * sin(time * 2.0 + position.x * 8.0)); | |
| vPosition = position; | |
| // Organic color blending | |
| float colorMix = sin(time * 1.5 + position.y * 6.0) * 0.5 + 0.5; | |
| vColor = mix(primaryColor, secondaryColor, colorMix); | |
| vColor = mix(vColor, highlightColor, audioEnergy * 0.3); | |
| vec3 pos = position; | |
| // Add organic growth undulation | |
| float growth = min(1.0, growthProgress + sin(time * 0.5) * 0.1); | |
| pos *= growth; | |
| // Audio-reactive pulsing | |
| pos += 0.01 * audioEnergy * sin(time * 4.0 + length(position) * 12.0) * normalize(position); | |
| gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0); | |
| } | |
| `, | |
| fragmentShader: ` | |
| varying float vAlpha; | |
| varying vec3 vColor; | |
| varying vec3 vPosition; | |
| uniform float vineGlow; | |
| void main() { | |
| // Distance-based glow effect | |
| float dist = length(vPosition); | |
| float glow = 1.0 / (1.0 + dist * 2.0); | |
| glow = pow(glow, vineGlow); | |
| // Organic pulsing opacity | |
| float pulse = sin(dist * 8.0 - time * 3.0) * 0.3 + 0.7; | |
| gl_FragColor = vec4(vColor * glow, vAlpha * pulse); | |
| } | |
| `, | |
| transparent: true, | |
| blending: THREE.AdditiveBlending, | |
| side: THREE.DoubleSide | |
| }) | |
| // Create vine line system | |
| const vineLines = new THREE.LineSegments(vineGeometry, vineMaterial) | |
| vineLines.userData = { type: 'dysonVines' } | |
| scene.add(vineLines) | |
| // Create node instances | |
| const nodeInstances = [] | |
| for (let i = 0; i < dysonConfig.nodes.nodeCount; i++) { | |
| const node = new THREE.Mesh(nodeGeometry, nodeMaterial.clone()) | |
| node.userData = { type: 'dysonNode', index: i } | |
| scene.add(node) | |
| nodeInstances.push(node) | |
| } | |
| // Initialize vine growth data | |
| const vineData = { | |
| vines: [] as any[], | |
| growthProgress: 0, | |
| lastUpdate: 0 | |
| } | |
| // Create initial vine seeds | |
| for (let i = 0; i < dysonConfig.growth.seedPoints; i++) { | |
| const theta = (i / dysonConfig.growth.seedPoints) * Math.PI * 2 | |
| const phi = Math.acos(2 * Math.random() - 1) | |
| const startPos = { | |
| x: sphereRadius * Math.sin(phi) * Math.cos(theta), | |
| y: sphereRadius * Math.sin(phi) * Math.sin(theta), | |
| z: sphereRadius * Math.cos(phi) | |
| } | |
| vineData.vines.push({ | |
| segments: [startPos], | |
| growth: 0, | |
| direction: { x: Math.random() - 0.5, y: Math.random() - 0.5, z: Math.random() - 0.5 }, | |
| branches: [], | |
| maturity: 0, | |
| energy: Math.random() | |
| }) | |
| } | |
| dysonSphereRef.current = { | |
| vineGeometry, | |
| vineMaterial, | |
| vineLines, | |
| nodeGeometry, | |
| nodeInstances, | |
| vinePositions, | |
| vineColors, | |
| vineOpacities, | |
| vineData, | |
| maxVines, | |
| maxSegments, | |
| sphereRadius, | |
| lastUpdate: 0, | |
| config: dysonConfig, | |
| colors | |
| } | |
| console.log('✅ Dyson sphere growing vines system created') | |
| }, [dysonSphereEnabled, vineComplexity]) | |
| // Create individual particle sphere | |
| const createParticleSphere = async ( | |
| THREE: any, | |
| scene: any, | |
| index: number, | |
| params: ParticleSystemParams | |
| ): Promise<ParticleSphere> => { | |
| // Create geometry and buffers | |
| const geometry = new THREE.BufferGeometry() | |
| const positions = new Float32Array(params.particleCount * 3) | |
| const colors = new Float32Array(params.particleCount * 3) | |
| const velocities = new Float32Array(params.particleCount * 3) | |
| const basePositions = new Float32Array(params.particleCount * 3) | |
| const lifetimes = new Float32Array(params.particleCount) | |
| const maxLifetimes = new Float32Array(params.particleCount) | |
| const beatEffects = new Float32Array(params.particleCount) | |
| // Initialize particles | |
| for (let i = 0; i < params.particleCount; i++) { | |
| const i3 = i * 3 | |
| const radius = THREE.MathUtils.lerp(0, params.sphereRadius, params.innerSphereRadius) | |
| const theta = Math.random() * Math.PI * 2 | |
| const phi = Math.acos(2 * Math.random() - 1) | |
| const r = Math.cbrt(Math.random()) * radius | |
| const x = r * Math.sin(phi) * Math.cos(theta) | |
| const y = r * Math.sin(phi) * Math.sin(theta) | |
| const z = r * Math.cos(phi) | |
| positions[i3] = x | |
| positions[i3 + 1] = y | |
| positions[i3 + 2] = z | |
| basePositions[i3] = x | |
| basePositions[i3 + 1] = y | |
| basePositions[i3 + 2] = z | |
| velocities[i3] = 0 | |
| velocities[i3 + 1] = 0 | |
| velocities[i3 + 2] = 0 | |
| const lt = Math.random() * params.particleLifetime | |
| lifetimes[i] = lt | |
| maxLifetimes[i] = lt | |
| beatEffects[i] = 0 | |
| } | |
| geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)) | |
| geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3)) | |
| // Create material with organic styling | |
| const material = new THREE.PointsMaterial({ | |
| size: params.particleSize * (0.8 + index * 0.05), // Subtle size variation per sphere | |
| vertexColors: true, | |
| transparent: true, | |
| opacity: 0.85 - index * 0.05, // Decreasing opacity for depth | |
| blending: THREE.AdditiveBlending, | |
| fog: true, | |
| sizeAttenuation: true // Natural size falloff with distance | |
| }) | |
| // Create particle system | |
| const particleSystem = new THREE.Points(geometry, material) | |
| particleSystem.visible = params.enabled | |
| particleSystem.userData = { sphereIndex: index } // Critical: Link to sphere | |
| scene.add(particleSystem) | |
| // Create sphere object | |
| const sphere: ParticleSphere = { | |
| index, | |
| params, | |
| positions, | |
| velocities, | |
| basePositions, | |
| lifetimes, | |
| maxLifetimes, | |
| beatEffects, | |
| colors, | |
| lastNoiseScale: params.noiseScale, | |
| lastValidVolume: 0, | |
| lastRotationSpeed: 0, | |
| peakDetection: { | |
| energyHistory: [], | |
| historyLength: 30, | |
| lastPeakTime: 0, | |
| minTimeBetweenPeaks: 200 | |
| } | |
| } | |
| // Update colors | |
| updateSphereColors(THREE, sphere) | |
| return sphere | |
| } | |
| // Update sphere colors with organic palette blending | |
| const updateSphereColors = (THREE: any, sphere: ParticleSphere) => { | |
| const color1 = new THREE.Color(sphere.params.colorStart) | |
| const color2 = new THREE.Color(sphere.params.colorEnd) | |
| // Load organic palette for enhanced blending | |
| const config = configLoader.getConfig() | |
| const organicPalette = (config as any)?.colors?.organicPalette | |
| for (let i = 0; i < sphere.params.particleCount; i++) { | |
| const t = i / sphere.params.particleCount | |
| // Base color interpolation | |
| let r = color1.r * (1 - t) + color2.r * t | |
| let g = color1.g * (1 - t) + color2.g * t | |
| let b = color1.b * (1 - t) + color2.b * t | |
| // Apply organic palette enhancement if available | |
| if (organicPalette && entronautEnabled) { | |
| const emergence = entronautStateRef.current?.informationMetrics?.emergence || 0 | |
| // Blend with organic colors based on SEFA emergence | |
| const organicInfluence = emergence * 0.3 | |
| // Choose organic color based on frequency range | |
| let organicColor | |
| if (sphere.index === 0) { // Sub-bass -> deep forest/bronze | |
| organicColor = hexToRgb(organicPalette.deepForest) || { r: 0.18, g: 0.31, b: 0.09 } | |
| } else if (sphere.index === 1) { // Bass -> earth brown/copper | |
| organicColor = hexToRgb(organicPalette.earthBrown) || { r: 0.54, g: 0.27, b: 0.07 } | |
| } else if (sphere.index === 2) { // Mid -> copper glow/gold | |
| organicColor = hexToRgb(organicPalette.copperGlow) || { r: 0.72, g: 0.45, b: 0.20 } | |
| } else if (sphere.index === 3) { // High-mid -> gold leaf/moss | |
| organicColor = hexToRgb(organicPalette.goldLeaf) || { r: 0.85, g: 0.65, b: 0.13 } | |
| } else { // High -> teal depth/amber | |
| organicColor = hexToRgb(organicPalette.tealDepth) || { r: 0.13, g: 0.70, b: 0.67 } | |
| } | |
| // Blend with organic color | |
| r = r * (1 - organicInfluence) + organicColor.r * organicInfluence | |
| g = g * (1 - organicInfluence) + organicColor.g * organicInfluence | |
| b = b * (1 - organicInfluence) + organicColor.b * organicInfluence | |
| } | |
| sphere.colors[i * 3] = r | |
| sphere.colors[i * 3 + 1] = g | |
| sphere.colors[i * 3 + 2] = b | |
| } | |
| } | |
| // Update fog | |
| const updateFog = (THREE: any) => { | |
| if (!sceneRef.current) return | |
| if (!fogParams.enabled) { | |
| sceneRef.current.fog = null | |
| } else { | |
| const color = new THREE.Color(fogParams.color) | |
| sceneRef.current.fog = new THREE.Fog(color, fogParams.near, fogParams.far) | |
| } | |
| } | |
| // Main animation loop | |
| const animate = useCallback((currentTime: number) => { | |
| if (!isVisualizationReady || !rendererRef.current || !sceneRef.current || !cameraRef.current) { | |
| animationRef.current = requestAnimationFrame(animate) | |
| return | |
| } | |
| const deltaTime = lastTimeRef.current ? (currentTime - lastTimeRef.current) / 1000 : 0 | |
| lastTimeRef.current = currentTime | |
| // Check if audio is playing and hide startup info | |
| if (showStartupInfo && audioEngine) { | |
| const audioData = audioEngine.getAudioData() | |
| if (audioData.overallAmplitude > 0.01) { | |
| setShowStartupInfo(false) | |
| } | |
| } | |
| // Enhanced FPS monitoring with auto-refresh triggers | |
| setPerformanceStats(prev => { | |
| const frameCount = prev.frameCount + 1 | |
| const timeSinceLastUpdate = currentTime - prev.lastFpsUpdate | |
| if (timeSinceLastUpdate > 1000) { // Update FPS every second | |
| const fps = Math.round(frameCount * 1000 / timeSinceLastUpdate) | |
| const totalParticles = spheresRef.current.reduce((sum, sphere) => | |
| sum + (sphere.params.enabled ? sphere.params.particleCount : 0), 0) | |
| // Hidden Auto-Refresh Logic | |
| const autoRefresh = autoRefreshRef.current | |
| const now = Date.now() | |
| // Check for performance degradation | |
| if (fps < 30) { | |
| autoRefresh.performanceDegradationCount++ | |
| } else { | |
| autoRefresh.performanceDegradationCount = Math.max(0, autoRefresh.performanceDegradationCount - 1) | |
| } | |
| // Check particle health every 5 seconds | |
| if (frameCount % 5 === 0) { | |
| autoRefresh.particleHealthChecks++ | |
| const isHealthy = checkParticleHealth() | |
| if (!isHealthy) { | |
| autoRefresh.emergencyResetCount++ | |
| } | |
| } | |
| // Trigger hidden refresh if conditions are met | |
| const timeSinceLastRefresh = now - autoRefresh.lastRefresh | |
| const shouldRefresh = ( | |
| // Periodic refresh (every 2 minutes base) | |
| timeSinceLastRefresh > autoRefresh.refreshInterval || | |
| // Performance degradation (3 consecutive low FPS readings) | |
| autoRefresh.performanceDegradationCount >= 3 || | |
| // Particle health issues (2 consecutive unhealthy checks) | |
| autoRefresh.emergencyResetCount >= 2 | |
| ) | |
| if (shouldRefresh && !autoRefresh.isRefreshing) { | |
| // Adjust refresh interval based on performance | |
| if (autoRefresh.performanceDegradationCount >= 3) { | |
| autoRefresh.refreshInterval = 60000 // 1 minute for poor performance | |
| } else if (fps > 45) { | |
| autoRefresh.refreshInterval = 180000 // 3 minutes for good performance | |
| } else { | |
| autoRefresh.refreshInterval = 120000 // 2 minutes default | |
| } | |
| // Perform refresh without blocking the main thread | |
| setTimeout(() => performHiddenRefresh(), 100) | |
| } | |
| return { | |
| fps, | |
| particleCount: totalParticles, | |
| lastFpsUpdate: currentTime, | |
| frameCount: 0 | |
| } | |
| } | |
| return { ...prev, frameCount } | |
| }) | |
| // Update camera controls | |
| if (controlsRef.current) { | |
| controlsRef.current.enabled = cameraControlsEnabled | |
| controlsRef.current.autoRotate = autoRotate && cameraControlsEnabled | |
| if (cameraControlsEnabled) { | |
| controlsRef.current.update() | |
| } | |
| } | |
| // Update beat manager | |
| beatManagerRef.current?.update(deltaTime) | |
| // ENTRONAUT: Analyze emergence patterns in audio data (reduced frequency for performance) | |
| if (audioEngine && entronautEnabled && (Math.floor(currentTime * 0.01) % 3 === 0)) { // Run every 3rd frame | |
| const audioData = audioEngine.getAudioData() | |
| if (Math.random() < 0.005) { // Reduced logging frequency | |
| console.log('🧠 SEFA Analysis - Audio Data:', { | |
| deepEnergy: audioData.deepEnergy, | |
| midEnergy: audioData.midEnergy, | |
| highEnergy: audioData.highEnergy, | |
| overallAmplitude: audioData.overallAmplitude, | |
| beatDetected: audioData.beatDetected | |
| }) | |
| } | |
| analyzeSymbolicEmergence(audioData, currentTime) | |
| } | |
| // Update particle spheres | |
| updateParticleSpheres(currentTime, deltaTime) | |
| // Update dynamic tendrils | |
| updateTendrilSystem(currentTime, deltaTime) | |
| // Update Dyson sphere/growing vines | |
| updateDysonSphere(currentTime, deltaTime) | |
| // Render scene | |
| rendererRef.current.render(sceneRef.current, cameraRef.current) | |
| animationRef.current = requestAnimationFrame(animate) | |
| }, [isVisualizationReady, audioEngine, entronautEnabled, analyzeSymbolicEmergence]) | |
| // Update all particle spheres with inter-sphere communication | |
| const updateParticleSpheres = (currentTime: number, deltaTime: number) => { | |
| if (!audioEngine || !noiseGeneratorRef.current || !beatManagerRef.current) return | |
| // Calculate global emergence state for inter-sphere communication | |
| const globalEmergence = entronautStateRef.current ? | |
| entronautStateRef.current.informationMetrics.emergence : 0 | |
| const globalComplexity = entronautStateRef.current ? | |
| entronautStateRef.current.informationMetrics.complexity : 0 | |
| spheresRef.current.forEach((sphere, sphereIndex) => { | |
| if (!sphere.params.enabled) return | |
| // Get audio data for this sphere | |
| const audioData = audioEngine.getAdvancedAudioData(sphere) | |
| // Add debug logging for the first sphere | |
| if (sphere.index === 0 && Math.random() < 0.01) { // 1% chance to log | |
| console.log('🎯 Audio data for sphere 0:', { | |
| rangeEnergy: audioData.rangeEnergy, | |
| rangeEnergyBeat: audioData.rangeEnergyBeat, | |
| peakDetected: audioData.peakDetected, | |
| beatThreshold: sphere.params.beatThreshold | |
| }) | |
| } | |
| // OPTIMIZED INTER-SPHERE COMMUNICATION (run less frequently) | |
| if (entronautEnabled && adaptiveCoupling && (sphereIndex === 0 || Math.random() < 0.1)) { | |
| // Simplified sphere influence with reduced frequency | |
| let avgNoiseScale = 0 | |
| let activeNeighbors = 0 | |
| spheresRef.current.forEach((otherSphere, otherIndex) => { | |
| if (otherIndex !== sphereIndex && otherSphere.params.enabled) { | |
| avgNoiseScale += otherSphere.params.noiseScale | |
| activeNeighbors++ | |
| } | |
| }) | |
| if (activeNeighbors > 0) { | |
| avgNoiseScale /= activeNeighbors | |
| const networkInfluence = globalEmergence * 0.1 // Reduced influence | |
| sphere.params.noiseScale = sphere.params.noiseScale * (1 - networkInfluence) + | |
| avgNoiseScale * networkInfluence | |
| } | |
| // Simplified synchronization | |
| const syncPulse = globalComplexity * 0.2 | |
| sphere.params.turbulenceStrength *= (1 + syncPulse) | |
| } | |
| // Handle peak detection and dynamic noise scaling | |
| if (audioData.peakDetected && sphere.params.dynamicNoiseScale) { | |
| sphere.params.noiseScale = generateNewNoiseScale(sphere.params, sphere.lastNoiseScale) | |
| sphere.lastNoiseScale = sphere.params.noiseScale | |
| } | |
| // Beat detection and wave triggering | |
| const beatDetected = audioData.rangeEnergyBeat > sphere.params.beatThreshold | |
| if (beatDetected && !beatManagerRef.current!.isWaveActive && sphere.params.beatStrength > 0) { | |
| beatManagerRef.current!.triggerWave(audioData.rangeEnergyBeat) | |
| } | |
| // Update particles | |
| updateSphereParticles(sphere, currentTime, deltaTime, beatDetected) | |
| // Update rotation based on audio | |
| updateSphereRotation(sphere, audioData) | |
| }) | |
| } | |
| // Update dynamic tendril system with performance optimization | |
| const updateTendrilSystem = (currentTime: number, _deltaTime: number) => { | |
| if (!tendrilsEnabled || !tendrilSystemRef.current || !audioEngine || !entronautStateRef.current) return | |
| // Performance optimization for tendril system | |
| const adaptiveLevel = 1.0 // Simplified - no performance monitoring | |
| const tendrilSystem = tendrilSystemRef.current | |
| const { positions, colors, opacities, maxTendrils, maxTendrilLength, tendrilSegments } = tendrilSystem | |
| // Update shader uniforms | |
| const audioData = audioEngine.getAudioData() | |
| const sefaMetrics = entronautStateRef.current.informationMetrics | |
| tendrilSystem.material.uniforms.time.value = currentTime * 0.001 | |
| tendrilSystem.material.uniforms.sefaField.value = sefaMetrics.emergence | |
| tendrilSystem.material.uniforms.audioEnergy.value = audioData.overallAmplitude || 0 | |
| // Only update connections periodically for performance | |
| if (currentTime - tendrilSystem.lastUpdate < 100) return // Update every 100ms | |
| tendrilSystem.lastUpdate = currentTime | |
| let tendrilIndex = 0 | |
| const performanceMultiplier = Math.max(0.2, adaptiveLevel) // Minimum 20% tendrils even at low performance | |
| const activeTendrils = Math.floor(maxTendrils * tendrilDensity * performanceMultiplier * (0.5 + 0.5 * audioData.overallAmplitude)) | |
| // Reset all positions | |
| positions.fill(0) | |
| opacities.fill(0) | |
| // Create connections between particles from different spheres | |
| for (let sphereA = 0; sphereA < spheresRef.current.length && tendrilIndex < activeTendrils; sphereA++) { | |
| const sphere1 = spheresRef.current[sphereA] | |
| if (!sphere1.params.enabled) continue | |
| for (let sphereB = sphereA + 1; sphereB < spheresRef.current.length && tendrilIndex < activeTendrils; sphereB++) { | |
| const sphere2 = spheresRef.current[sphereB] | |
| if (!sphere2.params.enabled) continue | |
| // Sample particles from each sphere | |
| const sampleCount = Math.min(10, Math.floor(sphere1.params.particleCount / 1000)) | |
| for (let i = 0; i < sampleCount && tendrilIndex < activeTendrils; i++) { | |
| const p1Index = Math.floor(Math.random() * sphere1.params.particleCount) | |
| const p2Index = Math.floor(Math.random() * sphere2.params.particleCount) | |
| const p1x = sphere1.positions[p1Index * 3] | |
| const p1y = sphere1.positions[p1Index * 3 + 1] | |
| const p1z = sphere1.positions[p1Index * 3 + 2] | |
| const p2x = sphere2.positions[p2Index * 3] | |
| const p2y = sphere2.positions[p2Index * 3 + 1] | |
| const p2z = sphere2.positions[p2Index * 3 + 2] | |
| const distance = Math.sqrt( | |
| (p2x - p1x) ** 2 + (p2y - p1y) ** 2 + (p2z - p1z) ** 2 | |
| ) | |
| // Only create tendril if particles are within range | |
| if (distance < maxTendrilLength) { | |
| // Calculate SEFA influence on tendril strength | |
| const fieldWidth = 32 | |
| const fieldX = Math.floor(((p1x + p2x) * 0.5 + 1) * 0.5 * fieldWidth) | |
| const fieldY = Math.floor(((p1y + p2y) * 0.5 + 1) * 0.5 * fieldWidth) | |
| const sefaIndex = Math.max(0, Math.min( | |
| entronautStateRef.current.sefaScore.length - 1, | |
| fieldY * fieldWidth + fieldX | |
| )) | |
| const sefaStrength = entronautStateRef.current.sefaScore[sefaIndex] | |
| // Create curved tendril with multiple segments | |
| for (let segment = 0; segment < tendrilSegments - 1; segment++) { | |
| const t1 = segment / (tendrilSegments - 1) | |
| const t2 = (segment + 1) / (tendrilSegments - 1) | |
| // Calculate curved path with SEFA-driven undulation | |
| const curve1 = getCurvedTendrilPoint(p1x, p1y, p1z, p2x, p2y, p2z, t1, currentTime, sefaStrength) | |
| const curve2 = getCurvedTendrilPoint(p1x, p1y, p1z, p2x, p2y, p2z, t2, currentTime, sefaStrength) | |
| const baseIndex = tendrilIndex * tendrilSegments * 6 + segment * 6 | |
| // Set positions for line segment | |
| positions[baseIndex] = curve1.x | |
| positions[baseIndex + 1] = curve1.y | |
| positions[baseIndex + 2] = curve1.z | |
| positions[baseIndex + 3] = curve2.x | |
| positions[baseIndex + 4] = curve2.y | |
| positions[baseIndex + 5] = curve2.z | |
| // Set colors based on frequency and SEFA with organic palette blending | |
| const colorIntensity = 0.4 + 0.6 * sefaStrength // More subtle intensity range | |
| const freq1Color = getFrequencyColor(sphere1.params.minFrequency, colorIntensity) | |
| const freq2Color = getFrequencyColor(sphere2.params.minFrequency, colorIntensity) | |
| // Blend colors organically | |
| const blendFactor = Math.sin(currentTime * 0.0008 + distance * 5) * 0.5 + 0.5 | |
| colors[baseIndex] = freq1Color.r * (1 - blendFactor) + freq2Color.r * blendFactor | |
| colors[baseIndex + 1] = freq1Color.g * (1 - blendFactor) + freq2Color.g * blendFactor | |
| colors[baseIndex + 2] = freq1Color.b * (1 - blendFactor) + freq2Color.b * blendFactor | |
| colors[baseIndex + 3] = freq2Color.r * (1 - blendFactor) + freq1Color.r * blendFactor | |
| colors[baseIndex + 4] = freq2Color.g * (1 - blendFactor) + freq1Color.g * blendFactor | |
| colors[baseIndex + 5] = freq2Color.b * (1 - blendFactor) + freq1Color.b * blendFactor | |
| // Set opacity based on audio energy and distance | |
| const opacity = (1 - distance / maxTendrilLength) * sefaStrength * audioData.overallAmplitude | |
| const opacityIndex = tendrilIndex * tendrilSegments * 2 + segment * 2 | |
| opacities[opacityIndex] = opacity | |
| opacities[opacityIndex + 1] = opacity | |
| } | |
| tendrilIndex++ | |
| } | |
| } | |
| } | |
| } | |
| // Mark geometry for update | |
| tendrilSystem.geometry.attributes.position.needsUpdate = true | |
| tendrilSystem.geometry.attributes.color.needsUpdate = true | |
| tendrilSystem.geometry.attributes.alpha.needsUpdate = true | |
| } | |
| // Update Dyson sphere/growing vines system with performance optimization | |
| const updateDysonSphere = (currentTime: number, deltaTime: number) => { | |
| if (!dysonSphereEnabled || !dysonSphereRef.current || !audioEngine) return | |
| // Performance optimization for Dyson sphere | |
| // Simplified - no dynamic update frequency | |
| const dysonSystem = dysonSphereRef.current | |
| const { vineMaterial, vinePositions, vineColors, vineOpacities, vineData, config, colors } = dysonSystem | |
| // Update shader uniforms | |
| const audioData = audioEngine.getAudioData() | |
| const sefaMetrics = entronautStateRef.current?.informationMetrics || { emergence: 0, complexity: 0 } | |
| vineMaterial.uniforms.time.value = currentTime * 0.001 | |
| vineMaterial.uniforms.audioEnergy.value = audioData.overallAmplitude || 0 | |
| vineMaterial.uniforms.emergenceField.value = sefaMetrics.emergence | |
| vineMaterial.uniforms.growthProgress.value = vineData.growthProgress | |
| // Only update vine growth periodically for performance | |
| if (currentTime - dysonSystem.lastUpdate < 50) return // Update every 50ms | |
| dysonSystem.lastUpdate = currentTime | |
| // Update vine growth | |
| vineData.growthProgress = Math.min(1.0, vineData.growthProgress + vineGrowthRate * deltaTime) | |
| let vineIndex = 0 | |
| // Reset all positions | |
| vinePositions.fill(0) | |
| vineOpacities.fill(0) | |
| // Update each vine | |
| vineData.vines.forEach((vine: any, vIndex: number) => { | |
| if (vineIndex >= dysonSystem.maxVines) return | |
| // Grow vine segments based on audio energy and SEFA | |
| const audioInfluence = (audioData.overallAmplitude || 0) * config.vines.audioReactivity | |
| const growthRate = config.vines.growthSpeed * (1 + audioInfluence) * vineData.growthProgress | |
| vine.growth += growthRate * deltaTime | |
| vine.maturity = Math.min(1.0, vine.maturity + deltaTime / config.growth.maturityTime) | |
| // Add new segments as vine grows | |
| if (vine.growth > 1.0 && vine.segments.length < config.vines.maxSegmentsPerVine) { | |
| vine.growth = 0 | |
| const lastSegment = vine.segments[vine.segments.length - 1] | |
| // Calculate organic growth direction | |
| const centerForce = { | |
| x: -lastSegment.x * 0.1, | |
| y: -lastSegment.y * 0.1, | |
| z: -lastSegment.z * 0.1 | |
| } | |
| // Add curvature and organic variation | |
| vine.direction.x += (Math.random() - 0.5) * config.vines.organicVariation + centerForce.x | |
| vine.direction.y += (Math.random() - 0.5) * config.vines.organicVariation + centerForce.y | |
| vine.direction.z += (Math.random() - 0.5) * config.vines.organicVariation + centerForce.z | |
| // Normalize direction | |
| const dirLength = Math.sqrt(vine.direction.x ** 2 + vine.direction.y ** 2 + vine.direction.z ** 2) | |
| if (dirLength > 0) { | |
| vine.direction.x /= dirLength | |
| vine.direction.y /= dirLength | |
| vine.direction.z /= dirLength | |
| } | |
| // Create new segment | |
| const segmentLength = 0.1 + sefaMetrics.emergence * 0.05 | |
| const newSegment = { | |
| x: lastSegment.x + vine.direction.x * segmentLength, | |
| y: lastSegment.y + vine.direction.y * segmentLength, | |
| z: lastSegment.z + vine.direction.z * segmentLength | |
| } | |
| vine.segments.push(newSegment) | |
| // Update node positions | |
| if (dysonSystem.nodeInstances[vIndex]) { | |
| dysonSystem.nodeInstances[vIndex].position.set(newSegment.x, newSegment.y, newSegment.z) | |
| // Update node material based on audio | |
| const nodeMat = dysonSystem.nodeInstances[vIndex].material as any | |
| nodeMat.opacity = 0.5 + 0.5 * audioData.overallAmplitude | |
| nodeMat.color.setHex(parseInt(colors.nodeColor.replace('#', ''), 16)) | |
| } | |
| } | |
| // Render vine segments | |
| for (let i = 0; i < vine.segments.length - 1 && vineIndex < dysonSystem.maxVines; i++) { | |
| const segment1 = vine.segments[i] | |
| const segment2 = vine.segments[i + 1] | |
| const baseIndex = vineIndex * 6 | |
| // Set positions for line segment | |
| vinePositions[baseIndex] = segment1.x | |
| vinePositions[baseIndex + 1] = segment1.y | |
| vinePositions[baseIndex + 2] = segment1.z | |
| vinePositions[baseIndex + 3] = segment2.x | |
| vinePositions[baseIndex + 4] = segment2.y | |
| vinePositions[baseIndex + 5] = segment2.z | |
| // Set colors based on maturity and audio | |
| const maturityColor = vine.maturity | |
| const audioColor = audioData.overallAmplitude || 0 | |
| const primaryColor = hexToRgb(colors.primaryVine) | |
| const secondaryColor = hexToRgb(colors.secondaryVine) | |
| const highlightColor = hexToRgb(colors.vineHighlight) | |
| if (primaryColor && secondaryColor && highlightColor) { | |
| const blendedColor = { | |
| r: primaryColor.r * (1 - maturityColor) + secondaryColor.r * maturityColor, | |
| g: primaryColor.g * (1 - maturityColor) + secondaryColor.g * maturityColor, | |
| b: primaryColor.b * (1 - maturityColor) + secondaryColor.b * maturityColor | |
| } | |
| // Apply audio highlight | |
| blendedColor.r = Math.min(1, blendedColor.r + highlightColor.r * audioColor * 0.3) | |
| blendedColor.g = Math.min(1, blendedColor.g + highlightColor.g * audioColor * 0.3) | |
| blendedColor.b = Math.min(1, blendedColor.b + highlightColor.b * audioColor * 0.3) | |
| vineColors[baseIndex] = blendedColor.r | |
| vineColors[baseIndex + 1] = blendedColor.g | |
| vineColors[baseIndex + 2] = blendedColor.b | |
| vineColors[baseIndex + 3] = blendedColor.r | |
| vineColors[baseIndex + 4] = blendedColor.g | |
| vineColors[baseIndex + 5] = blendedColor.b | |
| } | |
| // Set opacity based on growth and audio | |
| const opacity = vine.maturity * (0.6 + 0.4 * audioData.overallAmplitude) | |
| const opacityIndex = vineIndex * 2 | |
| vineOpacities[opacityIndex] = opacity | |
| vineOpacities[opacityIndex + 1] = opacity | |
| vineIndex++ | |
| } | |
| }) | |
| // Mark geometry for update | |
| dysonSystem.vineGeometry.attributes.position.needsUpdate = true | |
| dysonSystem.vineGeometry.attributes.color.needsUpdate = true | |
| dysonSystem.vineGeometry.attributes.alpha.needsUpdate = true | |
| } | |
| // Helper function to convert hex color to RGB | |
| const hexToRgb = (hex: string) => { | |
| const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex) | |
| return result ? { | |
| r: parseInt(result[1], 16) / 255, | |
| g: parseInt(result[2], 16) / 255, | |
| b: parseInt(result[3], 16) / 255 | |
| } : null | |
| } | |
| // Helper function to create curved tendril points | |
| const getCurvedTendrilPoint = (x1: number, y1: number, z1: number, x2: number, y2: number, z2: number, t: number, time: number, sefaStrength: number) => { | |
| // Linear interpolation | |
| const x = x1 + (x2 - x1) * t | |
| const y = y1 + (y2 - y1) * t | |
| const z = z1 + (z2 - z1) * t | |
| // Add curvature and SEFA-driven undulation | |
| const midPoint = t * (1 - t) * 4 // Peaks at t=0.5 | |
| const curve = midPoint * 0.2 * sefaStrength | |
| const undulation = 0.05 * sefaStrength * Math.sin(time * 0.002 + t * 10) | |
| return { | |
| x: x + curve * Math.sin(time * 0.001 + t * 5) + undulation, | |
| y: y + curve * Math.cos(time * 0.001 + t * 5) + undulation * 0.5, | |
| z: z + curve * Math.sin(time * 0.0015 + t * 3) + undulation * 0.3 | |
| } | |
| } | |
| // Helper function to get frequency-based color with organic palette | |
| const getFrequencyColor = (frequency: number, intensity: number) => { | |
| const config = configLoader.getConfig() | |
| const organicBlend = (config as any)?.colors?.tendrilColors?.organicBlend | |
| if (organicBlend) { | |
| const normalizedFreq = Math.min(1, frequency / 10000) | |
| // Use organic color blending | |
| let color | |
| if (normalizedFreq < 0.2) { | |
| color = organicBlend.forest // Deep greens for low frequencies | |
| } else if (normalizedFreq < 0.4) { | |
| color = organicBlend.bronze // Bronze for low-mid frequencies | |
| } else if (normalizedFreq < 0.6) { | |
| color = organicBlend.copper // Copper for mid frequencies | |
| } else if (normalizedFreq < 0.8) { | |
| color = organicBlend.gold // Gold for high-mid frequencies | |
| } else { | |
| color = organicBlend.teal // Teal for high frequencies | |
| } | |
| return { | |
| r: color[0] * intensity, | |
| g: color[1] * intensity, | |
| b: color[2] * intensity | |
| } | |
| } | |
| // Fallback to original calculation | |
| const normalizedFreq = Math.min(1, frequency / 10000) | |
| return { | |
| r: (0.8 + 0.2 * normalizedFreq) * intensity, | |
| g: (0.6 + 0.4 * Math.sin(normalizedFreq * Math.PI)) * intensity, | |
| b: (0.2 + 0.8 * (1 - normalizedFreq)) * intensity | |
| } | |
| } | |
| // Organic cymatic pattern generator - creates natural, sound-driven formations | |
| const getCymatePattern = useCallback((x: number, y: number, z: number, frequency: number, time: number) => { | |
| // Create organic patterns inspired by natural growth and cymatics | |
| const r = Math.sqrt(x*x + y*y + z*z) | |
| const theta = Math.atan2(y, x) | |
| const phi = Math.acos(z / (r || 1)) | |
| // Frequency-based pattern selection with organic variation | |
| const freqFactor = frequency / 1000 | |
| const pattern = Math.floor(freqFactor) % 5 // Added 5th pattern | |
| let patternX = 0, patternY = 0, patternZ = 0 | |
| // Load config for cymatic pattern settings | |
| const config = configLoader.getConfig() | |
| const cymateConfig = (config as any)?.cymatics?.patterns | |
| switch (pattern) { | |
| case 0: // Organic radial waves (tree rings, water ripples) | |
| const radialFreq = cymateConfig?.radialWaves?.frequencyFactor || 0.08 | |
| const radialPetals = cymateConfig?.radialWaves?.petalCount || 6 | |
| const radialPattern = Math.sin(r * radialFreq + time * 0.8) * Math.cos(theta * radialPetals + time * 0.3) | |
| patternX = radialPattern * Math.cos(theta) * 0.7 | |
| patternY = radialPattern * Math.sin(theta) * 0.7 | |
| patternZ = Math.sin(phi * 3 + time * 0.4) * (cymateConfig?.radialWaves?.amplitudeZ || 0.4) | |
| break | |
| case 1: // Natural spiral harmonics (nautilus, plant growth) | |
| const l = Math.max(1, Math.floor(freqFactor * 0.4) + 1) | |
| const m = Math.floor(freqFactor * 0.25) | |
| const spiralFactor = 1 + 0.3 * Math.sin(time * 0.6) | |
| patternX = Math.sin(l * phi * spiralFactor) * Math.cos(m * theta + time * 0.7) | |
| patternY = Math.sin(l * phi * spiralFactor) * Math.sin(m * theta + time * 0.7) | |
| patternZ = Math.cos(l * phi) * Math.sin(time * 1.2) * 0.6 | |
| break | |
| case 2: // Organic lattice (honeycomb, crystal growth) | |
| const latticeFreq = cymateConfig?.geometricLattice?.frequencyFactor || 0.15 | |
| const organic = 0.8 + 0.2 * Math.sin(time * 0.5) | |
| patternX = (Math.sin(x * latticeFreq + time * 0.6) + Math.sin(y * latticeFreq * 1.1)) * organic | |
| patternY = (Math.sin(y * latticeFreq + time * 0.8) + Math.sin(z * latticeFreq * 0.9)) * organic | |
| patternZ = (Math.sin(z * latticeFreq + time * 0.7) + Math.sin(x * latticeFreq * 1.05)) * organic | |
| break | |
| case 3: // Enhanced flower of life (sacred geometry) | |
| const flowerFreq = cymateConfig?.flowerOfLife?.frequencyFactor || 0.12 | |
| const basePetals = cymateConfig?.flowerOfLife?.basePetals || 6 | |
| const petalVariation = cymateConfig?.flowerOfLife?.petalVariation || 0.1 | |
| const flowerPetals = basePetals + Math.floor(freqFactor * petalVariation * 10) | |
| const bloom = 0.7 + 0.3 * Math.sin(time * 0.4) | |
| patternX = Math.sin(theta * flowerPetals + time * 0.5) * Math.sin(r * flowerFreq) * bloom | |
| patternY = Math.cos(theta * flowerPetals + time * 0.5) * Math.sin(r * flowerFreq) * bloom | |
| patternZ = Math.sin(phi * flowerPetals * 0.7 + time * 0.6) * (cymateConfig?.flowerOfLife?.amplitudeZ || 0.25) | |
| break | |
| case 4: // New: Organic vine growth pattern | |
| const vineFreq = freqFactor * 0.1 | |
| const vineSpiral = theta + r * 0.5 + time * 0.3 | |
| const growth = 0.6 + 0.4 * Math.sin(time * 0.35) | |
| patternX = Math.sin(vineSpiral) * Math.exp(-r * 0.2) * growth | |
| patternY = Math.cos(vineSpiral) * Math.exp(-r * 0.2) * growth | |
| patternZ = Math.sin(r * vineFreq + time * 0.5) * 0.4 * growth | |
| break | |
| } | |
| // Organic breathing effect with natural variation | |
| const breathing = cymateConfig?.breathing | |
| const breathingSpeed = breathing?.speed || 0.4 | |
| const intensityBase = breathing?.intensityBase || 0.25 | |
| const intensityVariation = breathing?.intensityVariation || 0.6 | |
| const intensity = intensityBase + intensityVariation * Math.sin(time * breathingSpeed) * | |
| (0.8 + 0.2 * Math.sin(time * breathingSpeed * 0.7)) // Natural variation | |
| return { | |
| x: patternX * intensity, | |
| y: patternY * intensity, | |
| z: patternZ * intensity | |
| } | |
| }, [cymateGeometry]) | |
| // Update individual sphere particles with performance optimization | |
| const updateSphereParticles = ( | |
| sphere: ParticleSphere, | |
| currentTime: number, | |
| deltaTime: number, | |
| beatDetected: boolean | |
| ) => { | |
| const { params, positions, velocities, lifetimes, beatEffects } = sphere | |
| const noise = noiseGeneratorRef.current! | |
| const beatManager = beatManagerRef.current! | |
| // Performance optimization: Skip heavy calculations if needed | |
| const adaptiveLevel = 1.0 // Simplified - no performance monitoring | |
| // Frame skipping for low performance - simplified | |
| // STABILITY: Clamp deltaTime to prevent integration instability | |
| const clampedDeltaTime = Math.min(deltaTime, 1/30) // Max 30 FPS equivalent timestep | |
| const config = configLoader.getConfig() | |
| const maxVelocity = config?.visualization?.physics?.maxVelocity || 0.1 | |
| const stabilityThreshold = config?.visualization?.physics?.stabilityThreshold || 10.0 | |
| // Adaptive particle count based on performance | |
| const adaptiveParticleCount = Math.min(params.particleCount, | |
| adaptiveLevel * 15000) // Simplified - no dynamic particle count | |
| // Level of detail calculation | |
| const particleUpdateStep = Math.max(1, Math.floor(1 / adaptiveLevel)) | |
| for (let i = 0; i < adaptiveParticleCount; i += particleUpdateStep) { | |
| const i3 = i * 3 | |
| let x = positions[i3] | |
| let y = positions[i3 + 1] | |
| let z = positions[i3 + 2] | |
| let vx = velocities[i3] | |
| let vy = velocities[i3 + 1] | |
| let vz = velocities[i3 + 2] | |
| let lt = lifetimes[i] | |
| let be = beatEffects[i] | |
| // STABILITY: Check for NaN or infinite values | |
| if (!isFinite(x) || !isFinite(y) || !isFinite(z) || | |
| !isFinite(vx) || !isFinite(vy) || !isFinite(vz)) { | |
| // Reset particle to safe position | |
| const theta = Math.random() * Math.PI * 2 | |
| const phi = Math.acos(2 * Math.random() - 1) | |
| const r = Math.cbrt(Math.random()) * params.sphereRadius * 0.5 | |
| x = r * Math.sin(phi) * Math.cos(theta) | |
| y = r * Math.sin(phi) * Math.sin(theta) | |
| z = r * Math.cos(phi) | |
| vx = vy = vz = 0 | |
| console.warn(`🚨 Particle ${i} reset due to invalid values`) | |
| } | |
| // STABILITY: Check distance from origin - emergency containment | |
| const distFromOrigin = Math.sqrt(x*x + y*y + z*z) | |
| if (distFromOrigin > stabilityThreshold) { | |
| // Pull particle back to safe zone | |
| const pullFactor = 0.1 | |
| x *= pullFactor | |
| y *= pullFactor | |
| z *= pullFactor | |
| vx *= 0.1 // Severe velocity damping | |
| vy *= 0.1 | |
| vz *= 0.1 | |
| console.warn(`🚨 Particle ${i} emergency reset - distance: ${distFromOrigin.toFixed(2)}`) | |
| } | |
| // Update lifetime | |
| lt -= clampedDeltaTime | |
| // Calculate distance and sphere radius early for use in forces | |
| const dist = Math.sqrt(x*x + y*y + z*z) | |
| const sphereRadius = params.sphereRadius | |
| // Define containment zones for use throughout particle update | |
| const coreZone = sphereRadius * 0.4 // Core zone - natural movement | |
| const innerZone = sphereRadius * 0.65 // Inner zone - gentle forces | |
| const boundaryZone = sphereRadius * 0.8 // Warning zone - medium forces | |
| const dangerZone = sphereRadius * 0.92 // Danger zone - strong forces | |
| const emergencyZone = sphereRadius * 0.98 // Emergency zone - very strong forces | |
| const hardBoundary = sphereRadius * 1.02 // Hard boundary - immediate reset | |
| // Apply noise-based turbulence with distance-based scaling | |
| const ns = params.noiseScale * adaptiveLevel // Scale noise with performance | |
| const speed = params.noiseSpeed | |
| const timeFactor = currentTime * 0.001 | |
| // Calculate noise values directly - no caching overhead | |
| const noiseValues = { | |
| x: noise.noise3D(x * ns + timeFactor * speed, y * ns, z * ns), | |
| y: noise.noise3D(x * ns, y * ns + timeFactor * speed, z * ns), | |
| z: noise.noise3D(x * ns, y * ns, z * ns + timeFactor * speed) | |
| } | |
| const noiseX = noiseValues.x | |
| const noiseY = noiseValues.y | |
| const noiseZ = noiseValues.z | |
| // STABILITY: Scale turbulence by performance, distance, and containment zones | |
| const performanceScale = Math.min(1.0, 60 * clampedDeltaTime) // Scale down for low FPS | |
| // Scale turbulence to maintain audio reactivity while preventing boundary escape | |
| const distanceScale = Math.max(0.3, 1 - (dist / sphereRadius) * 0.6) // Less aggressive reduction | |
| const turbulenceScale = params.turbulenceStrength * performanceScale * distanceScale * 0.9 // Preserve more turbulence | |
| vx += noiseX * turbulenceScale | |
| vy += noiseY * turbulenceScale | |
| vz += noiseZ * turbulenceScale | |
| // Apply beat effects | |
| if (beatDetected) { | |
| be = 1.0 | |
| } | |
| be *= 0.95 | |
| if (be > 0.01) { | |
| const dist = Math.sqrt(x*x + y*y + z*z) | |
| if (dist > 0) { | |
| const dx = x / dist | |
| const dy = y / dist | |
| const dz = z / dist | |
| // Modified beat force to prevent dispersion with stronger containment awareness | |
| const baseBeatForce = be * params.beatStrength * 0.8 // Increased to preserve audio reactivity | |
| // Use containment zones already defined above | |
| if (dist < coreZone) { | |
| // Normal outward beat force in core zone only | |
| const beatForce = baseBeatForce | |
| vx += dx * beatForce | |
| vy += dy * beatForce | |
| vz += dz * beatForce | |
| } else if (dist < innerZone) { | |
| // Reduced outward beat force in inner zone | |
| const beatForce = baseBeatForce * 0.6 | |
| vx += dx * beatForce | |
| vy += dy * beatForce | |
| vz += dz * beatForce | |
| } else if (dist < boundaryZone) { | |
| // Tangential beat force in boundary zone to add movement without outward push | |
| const tangentX = -dy | |
| const tangentY = dx | |
| const tangentZ = dz * 0.1 // Minimal Z movement | |
| const beatForce = baseBeatForce * 0.4 | |
| vx += tangentX * beatForce | |
| vy += tangentY * beatForce | |
| vz += tangentZ * beatForce | |
| } else if (dist < dangerZone) { | |
| // Inward beat force in danger zone to pull particles back | |
| const beatForce = baseBeatForce * 0.6 | |
| vx -= dx * beatForce | |
| vy -= dy * beatForce | |
| vz -= dz * beatForce | |
| } else { | |
| // Strong inward beat force beyond danger zone | |
| const beatForce = baseBeatForce * 1.2 | |
| vx -= dx * beatForce | |
| vy -= dy * beatForce | |
| vz -= dz * beatForce | |
| } | |
| } | |
| } | |
| // Apply wave forces with strong containment awareness | |
| const waveForce = beatManager.getWaveForce({ x, y, z }) | |
| if (waveForce > 0 && dist > 0) { | |
| const dx = x / dist | |
| const dy = y / dist | |
| const dz = z / dist | |
| // Balance wave force intensity to preserve audio reactivity while maintaining containment | |
| const baseWaveIntensity = 0.0006 // Increased to preserve audio reactivity | |
| // Use containment zones already defined above for wave behavior | |
| let waveIntensity = baseWaveIntensity | |
| let waveDirection = 1 // 1 = outward, -1 = inward, 0 = tangential | |
| if (dist < coreZone) { | |
| // Normal outward waves in core | |
| waveIntensity = baseWaveIntensity | |
| waveDirection = 1 | |
| } else if (dist < innerZone) { | |
| // Reduced outward waves in inner zone | |
| waveIntensity = baseWaveIntensity * 0.6 | |
| waveDirection = 1 | |
| } else if (dist < boundaryZone) { | |
| // Tangential waves in boundary zone | |
| waveIntensity = baseWaveIntensity * 0.3 | |
| // Create tangential force instead of radial | |
| const tangentX = -dy | |
| const tangentY = dx | |
| const tangentZ = 0 | |
| vx += tangentX * waveForce * waveIntensity | |
| vy += tangentY * waveForce * waveIntensity | |
| vz += tangentZ * waveForce * waveIntensity | |
| waveDirection = 0 // Skip radial application below | |
| } else { | |
| // Inward waves beyond boundary zone | |
| waveIntensity = baseWaveIntensity * 0.8 | |
| waveDirection = -1 | |
| } | |
| // Apply radial wave force if not tangential | |
| if (waveDirection !== 0) { | |
| vx += dx * waveForce * waveIntensity * waveDirection | |
| vy += dy * waveForce * waveIntensity * waveDirection | |
| vz += dz * waveForce * waveIntensity * waveDirection | |
| } | |
| } | |
| // Cymatic geometry patterns - sound-driven geometric formations with containment awareness | |
| if (cymateGeometry && audioEngine) { | |
| const freq = sphere.params.minFrequency + | |
| (sphere.params.maxFrequency - sphere.params.minFrequency) * 0.5 | |
| // Create geometric patterns based on frequency | |
| const geometricPattern = getCymatePattern(x, y, z, freq, currentTime * 0.001) | |
| // Scale cymatic intensity to balance audio reactivity with containment | |
| let cymateScale = 1.0 | |
| if (dist > boundaryZone) { | |
| // Moderate reduction near boundaries while preserving some reactivity | |
| cymateScale = 0.4 | |
| } else if (dist > coreZone) { | |
| // Gentle reduction outside core | |
| cymateScale = 1.0 - ((dist - coreZone) / (boundaryZone - coreZone)) * 0.4 | |
| } | |
| // Maintain stronger cymatic intensity for audio reactivity | |
| const organicIntensity = 0.0012 * cymateScale * (0.8 + 0.2 * Math.sin(currentTime * 0.0005)) | |
| vx += geometricPattern.x * organicIntensity | |
| vy += geometricPattern.y * organicIntensity | |
| vz += geometricPattern.z * organicIntensity | |
| } | |
| // Update positions | |
| x += vx | |
| y += vy | |
| z += vz | |
| // ENTRONAUT: Apply enhanced biomimetic coupling parameters | |
| const entronautParams = applyEntronautCoupling(sphere, i) | |
| // Optimized living entity behavior: adaptive computational load | |
| const entronautStep = Math.max(10, Math.floor(30 / adaptiveLevel)) // More aggressive stepping at low performance | |
| if (entronautEnabled && adaptiveCoupling && (i % entronautStep === 0)) { // Adaptive step size | |
| // Simplified flocking behavior with fewer neighbor checks | |
| let avgVx = 0, avgVy = 0, avgVz = 0 | |
| let neighborCount = 0 | |
| const flockRadius = 0.25 | |
| // Check only a few nearby particles for performance | |
| for (let j = Math.max(0, i - 20); j < Math.min(params.particleCount, i + 20); j += 5) { | |
| if (j === i) continue | |
| const j3 = j * 3 | |
| const dx = positions[j3] - x | |
| const dy = positions[j3 + 1] - y | |
| const dz = positions[j3 + 2] - z | |
| const distSq = dx*dx + dy*dy + dz*dz // Use squared distance to avoid sqrt | |
| if (distSq < flockRadius * flockRadius && distSq > 0) { | |
| avgVx += velocities[j3] | |
| avgVy += velocities[j3 + 1] | |
| avgVz += velocities[j3 + 2] | |
| neighborCount++ | |
| } | |
| } | |
| if (neighborCount > 0) { | |
| avgVx /= neighborCount | |
| avgVy /= neighborCount | |
| avgVz /= neighborCount | |
| // Reduce flocking influence to prevent convergence and preserve individuality | |
| const flockInfluence = entronautParams.coupling * 0.08 // Reduced from 0.2 | |
| // Add some randomness to promote emergence rather than convergence | |
| const emergenceRandomness = (Math.random() - 0.5) * 0.001 | |
| vx += (avgVx - vx) * flockInfluence + emergenceRandomness | |
| vy += (avgVy - vy) * flockInfluence + emergenceRandomness | |
| vz += (avgVz - vz) * flockInfluence + emergenceRandomness | |
| } | |
| } | |
| // Light metabolic pulsing to add organic variation without dampening | |
| if (entronautEnabled && adaptiveCoupling) { | |
| const metabolicPulse = entronautParams.diffusion * 0.3 // Reduced from 0.5 | |
| const timeVariation = currentTime * 0.0001 // Add time-based variation | |
| vx += metabolicPulse * Math.cos(i * 0.01 + timeVariation) | |
| vy += metabolicPulse * Math.sin(i * 0.01 + timeVariation) | |
| vz += metabolicPulse * Math.sin(i * 0.007 + timeVariation) * 0.5 // Add Z variation | |
| } | |
| // STABILITY: Velocity clamping to prevent runaway particles | |
| const velocity = Math.sqrt(vx*vx + vy*vy + vz*vz) | |
| if (velocity > maxVelocity) { | |
| const scale = maxVelocity / velocity | |
| vx *= scale | |
| vy *= scale | |
| vz *= scale | |
| } | |
| // Apply adaptive drag/damping with preservation of audio reactivity | |
| let dampingFactor = entronautParams.damping | |
| // Only apply extra damping in extreme zones to preserve energy | |
| if (dist > emergencyZone) { | |
| dampingFactor *= 0.9 // Light extra damping only near hard boundary | |
| } | |
| // Use less aggressive velocity decay to maintain particle liveliness | |
| const velocityDecay = config?.visualization?.physics?.velocityDecay || 0.998 // Increased from 0.995 | |
| dampingFactor = Math.max(dampingFactor, velocityDecay) // Use max to preserve energy | |
| vx *= dampingFactor | |
| vy *= dampingFactor | |
| vz *= dampingFactor | |
| // ENHANCED MULTI-LAYER CONTAINMENT SYSTEM | |
| // (dist, sphereRadius, and containment zones already calculated above) | |
| // HARD BOUNDARY - Immediate containment for escaped particles | |
| if (dist > hardBoundary) { | |
| // Immediately pull particle back to safe zone | |
| const pullbackFactor = 0.7 // Pull back to 70% of sphere radius | |
| const safeRadius = sphereRadius * pullbackFactor | |
| const safeX = (x / dist) * safeRadius | |
| const safeY = (y / dist) * safeRadius | |
| const safeZ = (z / dist) * safeRadius | |
| x = safeX | |
| y = safeY | |
| z = safeZ | |
| // Zero velocities for escaped particles | |
| vx *= 0.1 | |
| vy *= 0.1 | |
| vz *= 0.1 | |
| console.warn(`🚨 Particle ${i} hard reset - distance: ${dist.toFixed(2)}`) | |
| } | |
| // EMERGENCY ZONE - Very strong containment | |
| else if (dist > emergencyZone && dist > 0) { | |
| const dx = x / dist | |
| const dy = y / dist | |
| const dz = z / dist | |
| // Very strong inward force | |
| const emergencyForce = (dist - emergencyZone) / (hardBoundary - emergencyZone) * 0.025 | |
| vx -= dx * emergencyForce | |
| vy -= dy * emergencyForce | |
| vz -= dz * emergencyForce | |
| // Strong velocity damping | |
| const dampingFactor = 0.7 | |
| vx *= dampingFactor | |
| vy *= dampingFactor | |
| vz *= dampingFactor | |
| } | |
| // DANGER ZONE - Strong containment | |
| else if (dist > dangerZone && dist > 0) { | |
| const dx = x / dist | |
| const dy = y / dist | |
| const dz = z / dist | |
| // Strong inward force | |
| const dangerForce = (dist - dangerZone) / (emergencyZone - dangerZone) * 0.015 | |
| vx -= dx * dangerForce | |
| vy -= dy * dangerForce | |
| vz -= dz * dangerForce | |
| // Medium velocity damping | |
| const dampingFactor = 0.85 | |
| vx *= dampingFactor | |
| vy *= dampingFactor | |
| vz *= dampingFactor | |
| } | |
| // BOUNDARY ZONE - Medium containment | |
| else if (dist > boundaryZone && dist > 0) { | |
| const dx = x / dist | |
| const dy = y / dist | |
| const dz = z / dist | |
| // Medium inward force | |
| const boundaryForce = (dist - boundaryZone) / (dangerZone - boundaryZone) * 0.008 | |
| vx -= dx * boundaryForce | |
| vy -= dy * boundaryForce | |
| vz -= dz * boundaryForce | |
| // Light velocity damping | |
| const dampingFactor = 0.92 | |
| vx *= dampingFactor | |
| vy *= dampingFactor | |
| vz *= dampingFactor | |
| } | |
| // INNER ZONE - Gentle containment | |
| else if (dist > innerZone && dist > 0) { | |
| const dx = x / dist | |
| const dy = y / dist | |
| const dz = z / dist | |
| // Gentle inward force | |
| const innerForce = (dist - innerZone) / (boundaryZone - innerZone) * 0.003 | |
| vx -= dx * innerForce | |
| vy -= dy * innerForce | |
| vz -= dz * innerForce | |
| } | |
| // GENTLE CENTER BIAS - Only for particles very far from center | |
| if (dist > sphereRadius * 0.9 && dist > 0) { | |
| const dx = x / dist | |
| const dy = y / dist | |
| const dz = z / dist | |
| // Very gentle pull only when approaching boundaries | |
| const centerBias = (dist - sphereRadius * 0.9) / (sphereRadius * 0.1) * 0.0008 | |
| vx -= dx * centerBias | |
| vy -= dy * centerBias | |
| vz -= dz * centerBias | |
| } | |
| // VELOCITY CLAMPING - Less restrictive to preserve audio reactivity | |
| let maxVel = config?.visualization?.physics?.maxVelocity || 0.025 // Increased base velocity | |
| if (dist > emergencyZone) { | |
| maxVel *= 0.6 // Moderate limit only in emergency zone | |
| } else if (dist > dangerZone) { | |
| maxVel *= 0.8 // Light limit in danger zone | |
| } | |
| const velMag = Math.sqrt(vx*vx + vy*vy + vz*vz) | |
| if (velMag > maxVel) { | |
| const scale = maxVel / velMag | |
| vx *= scale | |
| vy *= scale | |
| vz *= scale | |
| } | |
| // Respawn dead particles | |
| if (lt <= 0) { | |
| const radius = params.sphereRadius * params.innerSphereRadius | |
| const theta = Math.random() * Math.PI * 2 | |
| const phi = Math.acos(2 * Math.random() - 1) | |
| const r = Math.cbrt(Math.random()) * radius | |
| x = r * Math.sin(phi) * Math.cos(theta) | |
| y = r * Math.sin(phi) * Math.sin(theta) | |
| z = r * Math.cos(phi) | |
| vx = 0 | |
| vy = 0 | |
| vz = 0 | |
| lt = Math.random() * params.particleLifetime | |
| be = 0 | |
| } | |
| // Final stability check before updating arrays | |
| const finalDist = Math.sqrt(x*x + y*y + z*z) | |
| const finalVel = Math.sqrt(vx*vx + vy*vy + vz*vz) | |
| // Emergency stability check | |
| if (finalDist > sphereRadius * 1.5 || finalVel > 0.1 || !isFinite(finalDist) || !isFinite(finalVel)) { | |
| // Emergency reset - place particle safely in core zone | |
| const safeRadius = sphereRadius * 0.3 | |
| const theta = Math.random() * Math.PI * 2 | |
| const phi = Math.acos(2 * Math.random() - 1) | |
| const r = Math.cbrt(Math.random()) * safeRadius | |
| x = r * Math.sin(phi) * Math.cos(theta) | |
| y = r * Math.sin(phi) * Math.sin(theta) | |
| z = r * Math.cos(phi) | |
| vx = 0 | |
| vy = 0 | |
| vz = 0 | |
| console.warn(`🚨 Emergency stability reset for particle ${i} - dist: ${finalDist.toFixed(2)}, vel: ${finalVel.toFixed(3)}`) | |
| } | |
| // Update arrays | |
| positions[i3] = x | |
| positions[i3 + 1] = y | |
| positions[i3 + 2] = z | |
| velocities[i3] = vx | |
| velocities[i3 + 1] = vy | |
| velocities[i3 + 2] = vz | |
| lifetimes[i] = lt | |
| beatEffects[i] = be | |
| } | |
| // Mark geometry for update | |
| if (sceneRef.current) { | |
| const particleSystem = sceneRef.current.children.find((child: any) => | |
| child.userData?.sphereIndex === sphere.index | |
| ) as any | |
| if (particleSystem?.geometry) { | |
| particleSystem.geometry.attributes.position.needsUpdate = true | |
| } | |
| } | |
| } | |
| // Update sphere rotation based on audio | |
| const updateSphereRotation = (sphere: ParticleSphere, _audioData: any) => { | |
| if (!audioEngine) return | |
| const volumeData = audioEngine.getSmoothVolume( | |
| sphere.lastValidVolume, | |
| sphere.params.volumeChangeThreshold | |
| ) | |
| if (volumeData.shouldUpdate) { | |
| const targetRotationSpeed = sphere.params.rotationSpeedMin + | |
| (sphere.params.rotationSpeedMax - sphere.params.rotationSpeedMin) * volumeData.volume | |
| sphere.lastRotationSpeed = sphere.params.rotationSpeed + | |
| (targetRotationSpeed - sphere.params.rotationSpeed) * sphere.params.rotationSmoothness | |
| sphere.lastValidVolume = volumeData.volume | |
| } | |
| // Apply rotation | |
| if (sceneRef.current) { | |
| const particleSystem = sceneRef.current.children.find((child: any) => | |
| child.userData?.sphereIndex === sphere.index | |
| ) as any | |
| if (particleSystem) { | |
| particleSystem.rotation.y += sphere.lastRotationSpeed | |
| } | |
| } | |
| } | |
| // Camera control functions | |
| const resetCameraPosition = useCallback(() => { | |
| if (!cameraRef.current || !controlsRef.current) return | |
| cameraRef.current.position.set(0, 0, 2.5) | |
| controlsRef.current.target.set(0, 0, 0) | |
| controlsRef.current.update() | |
| }, []) | |
| const setCameraPreset = useCallback((preset: 'default' | 'inside' | 'far' | 'top' | 'side') => { | |
| if (!cameraRef.current || !controlsRef.current) return | |
| switch (preset) { | |
| case 'default': | |
| cameraRef.current.position.set(0, 0, 2.5) | |
| break | |
| case 'inside': | |
| cameraRef.current.position.set(0, 0, 0.5) | |
| break | |
| case 'far': | |
| cameraRef.current.position.set(0, 0, 8) | |
| break | |
| case 'top': | |
| cameraRef.current.position.set(0, 5, 0) | |
| break | |
| case 'side': | |
| cameraRef.current.position.set(5, 0, 0) | |
| break | |
| } | |
| controlsRef.current.target.set(0, 0, 0) | |
| controlsRef.current.update() | |
| }, []) | |
| // Handle window resize | |
| const handleResize = useCallback(() => { | |
| if (!cameraRef.current || !rendererRef.current) return | |
| cameraRef.current.aspect = window.innerWidth / window.innerHeight | |
| cameraRef.current.updateProjectionMatrix() | |
| rendererRef.current.setSize(window.innerWidth, window.innerHeight) | |
| // Update controls on resize | |
| if (controlsRef.current) { | |
| controlsRef.current.handleResize?.() | |
| } | |
| }, []) | |
| // Initialize on mount | |
| useEffect(() => { | |
| initializeVisualization() | |
| window.addEventListener('resize', handleResize) | |
| // Listen for camera control disable events from audio dock | |
| const handleCameraControlToggle = (event: CustomEvent) => { | |
| if (controlsRef.current) { | |
| controlsRef.current.enabled = !event.detail && cameraControlsEnabled | |
| } | |
| } | |
| window.addEventListener('disableCameraControls', handleCameraControlToggle as EventListener) | |
| return () => { | |
| window.removeEventListener('resize', handleResize) | |
| window.removeEventListener('disableCameraControls', handleCameraControlToggle as EventListener) | |
| if (animationRef.current) { | |
| cancelAnimationFrame(animationRef.current) | |
| } | |
| if (controlsRef.current) { | |
| controlsRef.current.dispose() | |
| } | |
| } | |
| }, [initializeVisualization, handleResize, cameraControlsEnabled]) | |
| // Start animation when ready | |
| useEffect(() => { | |
| if (isVisualizationReady) { | |
| animationRef.current = requestAnimationFrame(animate) | |
| } | |
| return () => { | |
| if (animationRef.current) { | |
| cancelAnimationFrame(animationRef.current) | |
| } | |
| } | |
| }, [isVisualizationReady, animate]) | |
| return ( | |
| <div className="world-tree-visualizer"> | |
| <canvas | |
| ref={canvasRef} | |
| className="visualization-canvas" | |
| style={{ | |
| position: 'absolute', | |
| top: 0, | |
| left: 0, | |
| width: '100%', | |
| height: '100%', | |
| background: '#0D0A07', | |
| pointerEvents: 'auto' | |
| }} | |
| onMouseEnter={() => { | |
| // Enable camera controls when mouse enters visualization area | |
| if (controlsRef.current && cameraControlsEnabled) { | |
| controlsRef.current.enabled = true | |
| } | |
| }} | |
| /> | |
| {/* Compact Controls Panel */} | |
| <div | |
| className="particle-controls ancient-panel" | |
| style={{ | |
| position: 'fixed', | |
| bottom: '20px', | |
| right: '20px', | |
| zIndex: 1000, | |
| background: 'linear-gradient(135deg, var(--marble-dark), var(--teal-dark))', | |
| borderRadius: '8px', | |
| color: 'var(--copper-light)', | |
| fontFamily: 'Metamorphous, serif', | |
| maxWidth: '280px', | |
| maxHeight: '75vh', | |
| transition: 'all 0.4s ease-in-out', | |
| transform: controlsCollapsed ? 'translateX(calc(100% - 55px))' : 'translateX(0)', | |
| backdropFilter: 'blur(15px)', | |
| border: '2px solid var(--copper-medium)', | |
| pointerEvents: 'auto', | |
| boxShadow: '0 0 20px var(--copper-dark), inset 0 0 10px var(--teal-dark)', | |
| display: 'flex', | |
| flexDirection: 'column' | |
| }} | |
| onMouseEnter={() => { | |
| // Disable camera controls when mouse enters controls panel | |
| const event = new CustomEvent('disableCameraControls', { detail: true }) | |
| window.dispatchEvent(event) | |
| }} | |
| onMouseLeave={() => { | |
| // Re-enable camera controls when mouse leaves controls panel | |
| const event = new CustomEvent('disableCameraControls', { detail: false }) | |
| window.dispatchEvent(event) | |
| }} | |
| > | |
| {/* Collapse/Expand Toggle */} | |
| <button | |
| onClick={() => setControlsCollapsed(!controlsCollapsed)} | |
| onMouseEnter={(e) => { | |
| e.currentTarget.style.transform = 'translateY(-50%) scale(1.1)' | |
| e.currentTarget.style.borderColor = 'var(--transmission-glow)' | |
| e.currentTarget.style.boxShadow = '0 0 15px var(--transmission-glow)' | |
| }} | |
| onMouseLeave={(e) => { | |
| e.currentTarget.style.transform = 'translateY(-50%) scale(1)' | |
| e.currentTarget.style.borderColor = 'var(--copper-medium)' | |
| e.currentTarget.style.boxShadow = '0 0 5px var(--copper-bright)' | |
| }} | |
| style={{ | |
| position: 'absolute', | |
| left: controlsCollapsed ? '8px' : '-30px', | |
| top: '50%', | |
| transform: 'translateY(-50%)', | |
| background: 'linear-gradient(135deg, var(--teal-dark), var(--marble-dark))', | |
| border: '2px solid var(--copper-medium)', | |
| borderRadius: '50%', | |
| width: '28px', | |
| height: '28px', | |
| color: 'var(--copper-bright)', | |
| cursor: 'pointer', | |
| fontSize: '12px', | |
| display: 'flex', | |
| alignItems: 'center', | |
| justifyContent: 'center', | |
| backdropFilter: 'blur(15px)', | |
| transition: 'all 0.4s ease-in-out', | |
| zIndex: 1001, | |
| fontFamily: 'Uncial Antiqua, serif', | |
| boxShadow: '0 0 5px var(--copper-bright)' | |
| }} | |
| title={controlsCollapsed ? 'Expand Transmission Interface' : 'Collapse Interface'} | |
| > | |
| {controlsCollapsed ? '⟨' : '⟩'} | |
| </button> | |
| {/* Collapsed State Indicator */} | |
| {controlsCollapsed && ( | |
| <div style={{ | |
| padding: '12px 8px', | |
| textAlign: 'center', | |
| fontSize: '9px', | |
| lineHeight: '1.1', | |
| opacity: 1, | |
| transition: 'opacity 0.3s ease-in-out 0.2s', | |
| fontFamily: 'Uncial Antiqua, serif' | |
| }}> | |
| <div className="rune" style={{ fontSize: '14px', color: 'var(--copper-bright)' }}>⟐</div> | |
| <div style={{ fontSize: '7px', marginTop: '2px', color: 'var(--copper-medium)' }}> | |
| entity<br/>interface | |
| </div> | |
| </div> | |
| )} | |
| {/* Full Controls Panel */} | |
| <div style={{ | |
| padding: controlsCollapsed ? '0' : '20px 20px 0 20px', | |
| opacity: controlsCollapsed ? 0 : 1, | |
| transition: 'opacity 0.3s ease-in-out 0.1s', | |
| pointerEvents: controlsCollapsed ? 'none' : 'auto', | |
| maxHeight: controlsCollapsed ? '0' : 'auto', | |
| overflow: 'hidden', | |
| flexShrink: 0 | |
| }}> | |
| <h3 className="ancient-title" style={{ | |
| marginBottom: '10px', | |
| fontSize: '16px', | |
| textAlign: 'center', | |
| fontFamily: 'Metamorphous, serif' | |
| }}> | |
| <span className="rune">⟐</span> Liminal Sessions <span className="rune">⟐</span> | |
| </h3> | |
| <div className="transmission-text" style={{ | |
| textAlign: 'center', | |
| fontSize: '9px', | |
| marginBottom: '15px', | |
| opacity: 0.9, | |
| fontFamily: 'Uncial Antiqua, serif' | |
| }}> | |
| ≈ intercepted transmission ≈<br/> | |
| ◦ audio-reactive entity ◦ | |
| </div> | |
| </div> | |
| {/* Scrollable Content Area */} | |
| <div style={{ | |
| padding: controlsCollapsed ? '0' : '0 20px 20px 20px', | |
| opacity: controlsCollapsed ? 0 : 1, | |
| transition: 'opacity 0.3s ease-in-out 0.1s', | |
| pointerEvents: controlsCollapsed ? 'none' : 'auto', | |
| maxHeight: controlsCollapsed ? '0' : 'calc(75vh - 120px)', | |
| overflow: controlsCollapsed ? 'hidden' : 'auto', | |
| overflowY: controlsCollapsed ? 'hidden' : 'scroll', | |
| flexGrow: 1, | |
| // Custom scrollbar styling for better aesthetics | |
| scrollbarWidth: 'thin', | |
| scrollbarColor: 'var(--copper-medium) transparent' | |
| }} | |
| // Custom webkit scrollbar styles | |
| onMouseEnter={(e) => { | |
| e.currentTarget.style.setProperty('--webkit-scrollbar-width', '6px') | |
| e.currentTarget.style.setProperty('--webkit-scrollbar-track-background', 'transparent') | |
| e.currentTarget.style.setProperty('--webkit-scrollbar-thumb-background', 'var(--copper-medium)') | |
| e.currentTarget.style.setProperty('--webkit-scrollbar-thumb-border-radius', '3px') | |
| }} | |
| > | |
| <style>{` | |
| .particle-controls::-webkit-scrollbar { | |
| width: 6px; | |
| } | |
| .particle-controls::-webkit-scrollbar-track { | |
| background: transparent; | |
| } | |
| .particle-controls::-webkit-scrollbar-thumb { | |
| background: var(--copper-medium); | |
| border-radius: 3px; | |
| } | |
| .particle-controls::-webkit-scrollbar-thumb:hover { | |
| background: var(--copper-bright); | |
| } | |
| `}</style> | |
| <div style={{ marginBottom: '10px' }}> | |
| <label className="runic-text" style={{ | |
| display: 'block', | |
| marginBottom: '5px', | |
| fontSize: '11px', | |
| color: 'var(--copper-bright)' | |
| }}> | |
| <span className="rune">◯</span> active entities: {activeSpheresCount} | |
| </label> | |
| <input | |
| type="range" | |
| min="1" | |
| max="5" | |
| value={activeSpheresCount} | |
| onChange={(e) => { | |
| const count = parseInt(e.target.value) | |
| setActiveSpheresCount(count) | |
| spheresRef.current.forEach((sphere, index) => { | |
| sphere.params.enabled = index < count | |
| if (sceneRef.current) { | |
| const particleSystem = sceneRef.current.children.find((child: any) => | |
| child.userData?.sphereIndex === sphere.index | |
| ) as any | |
| if (particleSystem) { | |
| particleSystem.visible = sphere.params.enabled | |
| } | |
| } | |
| }) | |
| }} | |
| style={{ width: '100%' }} | |
| /> | |
| </div> | |
| <div style={{ marginBottom: '10px' }}> | |
| <label className="runic-text" style={{ | |
| display: 'block', | |
| marginBottom: '5px', | |
| fontSize: '11px', | |
| color: 'var(--copper-bright)' | |
| }}> | |
| <input | |
| type="checkbox" | |
| checked={cymateGeometry} | |
| onChange={(e) => setCymateGeometry(e.target.checked)} | |
| style={{ marginRight: '8px' }} | |
| /> | |
| <span className="rune">◈</span> cymatic patterns | |
| </label> | |
| </div> | |
| <div style={{ marginBottom: '10px' }}> | |
| <label className="runic-text" style={{ | |
| display: 'block', | |
| marginBottom: '5px', | |
| fontSize: '11px', | |
| color: 'var(--copper-bright)' | |
| }}> | |
| <input | |
| type="checkbox" | |
| checked={tendrilsEnabled} | |
| onChange={(e) => setTendrilsEnabled(e.target.checked)} | |
| style={{ marginRight: '8px' }} | |
| /> | |
| <span className="rune">⟨⟩</span> neural tendrils | |
| </label> | |
| </div> | |
| <div style={{ marginBottom: '10px' }}> | |
| <label className="runic-text" style={{ | |
| display: 'block', | |
| marginBottom: '5px', | |
| fontSize: '11px', | |
| color: 'var(--copper-bright)' | |
| }}> | |
| <span className="rune">≋</span> neural density: {Math.round(tendrilDensity * 100)}% | |
| </label> | |
| <input | |
| type="range" | |
| min="0.1" | |
| max="1.0" | |
| step="0.1" | |
| value={tendrilDensity} | |
| onChange={(e) => setTendrilDensity(parseFloat(e.target.value))} | |
| disabled={!tendrilsEnabled} | |
| style={{ width: '100%' }} | |
| /> | |
| </div> | |
| <div style={{ marginBottom: '10px' }}> | |
| <label className="runic-text" style={{ | |
| display: 'block', | |
| marginBottom: '5px', | |
| fontSize: '11px', | |
| color: 'var(--copper-bright)' | |
| }}> | |
| <input | |
| type="checkbox" | |
| checked={dysonSphereEnabled} | |
| onChange={(e) => setDysonSphereEnabled(e.target.checked)} | |
| style={{ marginRight: '8px' }} | |
| /> | |
| <span className="rune">🌿</span> growing vines | |
| </label> | |
| </div> | |
| <div style={{ marginBottom: '10px' }}> | |
| <label className="runic-text" style={{ | |
| display: 'block', | |
| marginBottom: '5px', | |
| fontSize: '11px', | |
| color: 'var(--copper-bright)' | |
| }}> | |
| <span className="rune">🌱</span> vine complexity: {vineComplexity} | |
| </label> | |
| <input | |
| type="range" | |
| min="8" | |
| max="48" | |
| step="4" | |
| value={vineComplexity} | |
| onChange={(e) => setVineComplexity(parseInt(e.target.value))} | |
| disabled={!dysonSphereEnabled} | |
| style={{ width: '100%' }} | |
| /> | |
| </div> | |
| <div style={{ marginBottom: '10px' }}> | |
| <label className="runic-text" style={{ | |
| display: 'block', | |
| marginBottom: '5px', | |
| fontSize: '11px', | |
| color: 'var(--copper-bright)' | |
| }}> | |
| <span className="rune">⟲</span> growth rate: {Math.round(vineGrowthRate * 1000)}% | |
| </label> | |
| <input | |
| type="range" | |
| min="0.005" | |
| max="0.05" | |
| step="0.005" | |
| value={vineGrowthRate} | |
| onChange={(e) => setVineGrowthRate(parseFloat(e.target.value))} | |
| disabled={!dysonSphereEnabled} | |
| style={{ width: '100%' }} | |
| /> | |
| </div> | |
| <div style={{ marginBottom: '10px' }}> | |
| <label className="runic-text" style={{ | |
| display: 'block', | |
| marginBottom: '5px', | |
| fontSize: '11px', | |
| color: 'var(--copper-bright)' | |
| }}> | |
| <input | |
| type="checkbox" | |
| checked={entronautEnabled} | |
| onChange={(e) => setEntronautEnabled(e.target.checked)} | |
| style={{ marginRight: '8px' }} | |
| /> | |
| <span className="rune">⟐</span> entronaut sefa | |
| </label> | |
| </div> | |
| <div style={{ marginBottom: '10px' }}> | |
| <label className="runic-text" style={{ | |
| display: 'block', | |
| marginBottom: '5px', | |
| fontSize: '11px', | |
| color: 'var(--copper-bright)' | |
| }}> | |
| <input | |
| type="checkbox" | |
| checked={adaptiveCoupling} | |
| onChange={(e) => setAdaptiveCoupling(e.target.checked)} | |
| disabled={!entronautEnabled || performanceMode} | |
| style={{ marginRight: '8px' }} | |
| /> | |
| <span className="rune">⧨</span> adaptive coupling | |
| </label> | |
| </div> | |
| <div style={{ marginBottom: '10px' }}> | |
| <label className="runic-text" style={{ | |
| display: 'block', | |
| marginBottom: '5px', | |
| fontSize: '11px', | |
| color: 'var(--copper-bright)' | |
| }}> | |
| <input | |
| type="checkbox" | |
| checked={performanceMode} | |
| onChange={(e) => { | |
| setPerformanceMode(e.target.checked) | |
| if (e.target.checked) { | |
| setAdaptiveCoupling(false) | |
| setEntronautEnabled(false) | |
| } | |
| }} | |
| style={{ marginRight: '8px' }} | |
| /> | |
| <span className="rune">⚡</span> performance mode | |
| </label> | |
| </div> | |
| {/* Performance Stats Display */} | |
| <div style={{ | |
| marginBottom: '10px', | |
| padding: '8px', | |
| background: 'rgba(0,0,0,0.3)', | |
| borderRadius: '4px', | |
| border: '1px solid var(--copper-dark)' | |
| }}> | |
| <div className="runic-text" style={{ | |
| fontSize: '9px', | |
| lineHeight: '1.3', | |
| color: 'var(--copper-medium)' | |
| }}> | |
| <div><span className="rune">⚡</span> quality: 100%</div> | |
| <div><span className="rune">🖥️</span> particles: {performanceStats.particleCount.toLocaleString()}</div> | |
| <div style={{ | |
| color: performanceStats.fps < 30 ? 'var(--error-rust)' : | |
| performanceStats.fps < 45 ? 'var(--warning-copper)' : 'var(--transmission-glow)' | |
| }}> | |
| <span className="rune">📊</span> performance: {performanceStats.fps >= 45 ? 'optimal' : performanceStats.fps >= 30 ? 'good' : 'stressed'} | |
| </div> | |
| </div> | |
| </div> | |
| {/* Manual Refresh Button */} | |
| <div style={{ marginBottom: '15px' }}> | |
| <button | |
| onClick={() => performHiddenRefresh(true)} | |
| disabled={autoRefreshRef.current.isRefreshing} | |
| style={{ | |
| width: '100%', | |
| padding: '8px 12px', | |
| background: autoRefreshRef.current.isRefreshing | |
| ? 'linear-gradient(135deg, var(--copper-dark), var(--teal-dark))' | |
| : 'linear-gradient(135deg, var(--marble-dark), var(--copper-medium))', | |
| border: '2px solid var(--copper-medium)', | |
| borderRadius: '6px', | |
| color: autoRefreshRef.current.isRefreshing ? 'var(--copper-medium)' : 'var(--copper-bright)', | |
| fontFamily: 'Uncial Antiqua, serif', | |
| fontSize: '10px', | |
| cursor: autoRefreshRef.current.isRefreshing ? 'not-allowed' : 'pointer', | |
| transition: 'all 0.3s ease-in-out', | |
| backdropFilter: 'blur(5px)', | |
| opacity: autoRefreshRef.current.isRefreshing ? 0.6 : 1, | |
| textShadow: '0 1px 2px rgba(0,0,0,0.5)' | |
| }} | |
| onMouseEnter={(e) => { | |
| if (!autoRefreshRef.current.isRefreshing) { | |
| e.currentTarget.style.borderColor = 'var(--transmission-glow)' | |
| e.currentTarget.style.background = 'linear-gradient(135deg, var(--copper-medium), var(--transmission-glow))' | |
| e.currentTarget.style.color = 'var(--marble-light)' | |
| e.currentTarget.style.transform = 'scale(1.02)' | |
| e.currentTarget.style.boxShadow = '0 4px 15px var(--transmission-glow)33' | |
| } | |
| }} | |
| onMouseLeave={(e) => { | |
| if (!autoRefreshRef.current.isRefreshing) { | |
| e.currentTarget.style.borderColor = 'var(--copper-medium)' | |
| e.currentTarget.style.background = 'linear-gradient(135deg, var(--marble-dark), var(--copper-medium))' | |
| e.currentTarget.style.color = 'var(--copper-bright)' | |
| e.currentTarget.style.transform = 'scale(1)' | |
| e.currentTarget.style.boxShadow = 'none' | |
| } | |
| }} | |
| > | |
| <span className="rune" style={{ marginRight: '6px' }}> | |
| {autoRefreshRef.current.isRefreshing ? '⧗' : '⟲'} | |
| </span> | |
| {autoRefreshRef.current.isRefreshing ? 'restoring equilibrium...' : 'reset particle fields'} | |
| </button> | |
| </div> | |
| <div style={{ marginBottom: '10px' }}> | |
| <label style={{ display: 'block', marginBottom: '5px', fontSize: '12px' }}> | |
| <input | |
| type="checkbox" | |
| checked={cameraControlsEnabled} | |
| onChange={(e) => setCameraControlsEnabled(e.target.checked)} | |
| style={{ marginRight: '8px' }} | |
| /> | |
| Camera Controls | |
| </label> | |
| </div> | |
| <div style={{ marginBottom: '10px' }}> | |
| <label style={{ display: 'block', marginBottom: '5px', fontSize: '12px' }}> | |
| <input | |
| type="checkbox" | |
| checked={autoRotate} | |
| onChange={(e) => setAutoRotate(e.target.checked)} | |
| disabled={!cameraControlsEnabled} | |
| style={{ marginRight: '8px' }} | |
| /> | |
| Auto Rotate | |
| </label> | |
| </div> | |
| {/* Camera Preset Buttons */} | |
| {cameraControlsEnabled && ( | |
| <div style={{ marginBottom: '10px' }}> | |
| <div style={{ fontSize: '11px', marginBottom: '5px', color: '#FFD700' }}>Camera Presets:</div> | |
| <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '4px' }}> | |
| <button | |
| onClick={() => setCameraPreset('default')} | |
| style={{ | |
| background: 'rgba(255, 215, 0, 0.2)', | |
| border: '1px solid rgba(255, 215, 0, 0.4)', | |
| color: '#FFD700', | |
| padding: '4px 8px', | |
| borderRadius: '4px', | |
| fontSize: '9px', | |
| cursor: 'pointer' | |
| }} | |
| > | |
| Default | |
| </button> | |
| <button | |
| onClick={() => setCameraPreset('inside')} | |
| style={{ | |
| background: 'rgba(255, 215, 0, 0.2)', | |
| border: '1px solid rgba(255, 215, 0, 0.4)', | |
| color: '#FFD700', | |
| padding: '4px 8px', | |
| borderRadius: '4px', | |
| fontSize: '9px', | |
| cursor: 'pointer' | |
| }} | |
| > | |
| Inside | |
| </button> | |
| <button | |
| onClick={() => setCameraPreset('far')} | |
| style={{ | |
| background: 'rgba(255, 215, 0, 0.2)', | |
| border: '1px solid rgba(255, 215, 0, 0.4)', | |
| color: '#FFD700', | |
| padding: '4px 8px', | |
| borderRadius: '4px', | |
| fontSize: '9px', | |
| cursor: 'pointer' | |
| }} | |
| > | |
| Far View | |
| </button> | |
| <button | |
| onClick={() => setCameraPreset('top')} | |
| style={{ | |
| background: 'rgba(255, 215, 0, 0.2)', | |
| border: '1px solid rgba(255, 215, 0, 0.4)', | |
| color: '#FFD700', | |
| padding: '4px 8px', | |
| borderRadius: '4px', | |
| fontSize: '9px', | |
| cursor: 'pointer' | |
| }} | |
| > | |
| Top View | |
| </button> | |
| </div> | |
| <button | |
| onClick={resetCameraPosition} | |
| style={{ | |
| background: 'rgba(255, 215, 0, 0.3)', | |
| border: '1px solid rgba(255, 215, 0, 0.5)', | |
| color: '#FFD700', | |
| padding: '4px 8px', | |
| borderRadius: '4px', | |
| fontSize: '9px', | |
| cursor: 'pointer', | |
| width: '100%', | |
| marginTop: '4px' | |
| }} | |
| > | |
| Reset Camera | |
| </button> | |
| </div> | |
| )} | |
| <div style={{ marginBottom: '10px' }}> | |
| <label style={{ display: 'block', marginBottom: '5px', fontSize: '12px' }}> | |
| <input | |
| type="checkbox" | |
| checked={fogParams.enabled} | |
| onChange={async (e) => { | |
| setFogParams(prev => ({ ...prev, enabled: e.target.checked })) | |
| const THREE = await import('three') | |
| updateFog(THREE) | |
| }} | |
| style={{ marginRight: '8px' }} | |
| /> | |
| Fog Effect | |
| </label> | |
| </div> | |
| <div className="transmission-text" style={{ | |
| fontSize: '9px', | |
| opacity: 0.9, | |
| marginTop: '15px', | |
| lineHeight: '1.4', | |
| fontFamily: 'Uncial Antiqua, serif', | |
| color: 'var(--copper-medium)' | |
| }}> | |
| <div>◦ audio-reactive ◦ | ≋ beat-detection ≋ | ⟐ dynamic-noise ⟐</div> | |
| <div style={{ marginTop: '5px' }}> | |
| <span className="rune">◯</span> {spheresRef.current.filter(s => s.params.enabled).length} entities | | |
| <span className="rune">✦</span> {performanceStats.particleCount.toLocaleString()} particles | |
| </div> | |
| <div style={{ | |
| marginTop: '5px', | |
| color: performanceStats.fps < 30 ? 'var(--error-rust)' : performanceStats.fps < 45 ? 'var(--warning-copper)' : 'var(--transmission-glow)' | |
| }}> | |
| <span className="rune">⧨</span> {performanceStats.fps} fps | transmission {performanceStats.fps >= 45 ? 'stable' : performanceStats.fps >= 30 ? 'fluctuating' : 'unstable'} | |
| </div> | |
| {cymateGeometry && <div style={{ marginTop: '3px' }}><span className="rune">◈</span> cymatic patterns active</div>} | |
| {tendrilsEnabled && <div style={{ marginTop: '3px' }}><span className="rune">⟨⟩</span> neural tendrils ({Math.round(tendrilDensity * 100)}% density)</div>} | |
| {dysonSphereEnabled && <div style={{ marginTop: '3px' }}><span className="rune">🌿</span> growing vines ({vineComplexity} complexity, {Math.round(vineGrowthRate * 1000)}% growth)</div>} | |
| {cameraControlsEnabled && ( | |
| <div style={{ marginTop: '3px', fontSize: '8px', color: 'var(--teal-light)' }}> | |
| <span className="rune">⟐</span> observer: orbit | zoom | pan | |
| </div> | |
| )} | |
| {entronautEnabled && ( | |
| <div style={{ marginTop: '5px', fontSize: '8px', lineHeight: '1.3' }}> | |
| <div><span className="rune">⟐</span> sefa analysis: {entronautStateRef.current ? 'active' : 'initializing'}</div> | |
| {entronautStateRef.current && ( | |
| <> | |
| <div>complexity: {(entronautStateRef.current.informationMetrics.complexity * 100).toFixed(1)}%</div> | |
| <div>emergence: {(entronautStateRef.current.informationMetrics.emergence * 100).toFixed(1)}%</div> | |
| <div>coherence: {(entronautStateRef.current.informationMetrics.coherence * 100).toFixed(1)}%</div> | |
| {adaptiveCoupling && ( | |
| <div style={{ marginTop: '3px', color: 'var(--teal-light)' }}> | |
| <span className="rune">⧨</span> biomimetic coupling active | |
| </div> | |
| )} | |
| </> | |
| )} | |
| </div> | |
| )} | |
| <div style={{ marginTop: '5px', fontSize: '8px', opacity: 0.8 }}> | |
| <div>sub-bass (20-80hz) | bass (120-250hz) | mid (250-800hz)</div> | |
| <div>high-mid (1k-4khz) | high (5k-10khz)</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {/* About Section - Expanded Clickable Widget */} | |
| <div | |
| className="about-section ancient-panel" | |
| onClick={() => setAboutCollapsed(!aboutCollapsed)} | |
| style={{ | |
| position: 'fixed', | |
| bottom: aboutCollapsed ? '20px' : '5%', | |
| left: aboutCollapsed ? '50%' : '50%', | |
| transform: aboutCollapsed ? 'translateX(-50%) translateY(calc(100% - 75px))' : 'translateX(-50%) translateY(0)', | |
| zIndex: 1000, | |
| background: 'linear-gradient(135deg, var(--marble-dark), var(--teal-dark))', | |
| borderRadius: aboutCollapsed ? '10px' : '15px', | |
| color: 'var(--copper-light)', | |
| fontFamily: 'Metamorphous, serif', | |
| width: aboutCollapsed ? '420px' : '90vw', | |
| minWidth: aboutCollapsed ? '420px' : '800px', | |
| maxWidth: aboutCollapsed ? '420px' : '1200px', | |
| height: aboutCollapsed ? 'auto' : '80vh', | |
| maxHeight: aboutCollapsed ? 'none' : '80vh', | |
| transition: 'all 0.5s ease-in-out', | |
| backdropFilter: 'blur(20px)', | |
| border: '2px solid var(--copper-medium)', | |
| pointerEvents: 'auto', | |
| boxShadow: aboutCollapsed | |
| ? '0 4px 20px var(--copper-dark), inset 0 0 15px var(--teal-dark)' | |
| : '0 10px 40px rgba(0, 0, 0, 0.7), inset 0 0 25px var(--teal-dark)', | |
| cursor: 'pointer', | |
| overflow: aboutCollapsed ? 'visible' : 'hidden' | |
| }} | |
| onMouseEnter={(e) => { | |
| // Disable camera controls when mouse enters about section | |
| const event = new CustomEvent('disableCameraControls', { detail: true }) | |
| window.dispatchEvent(event) | |
| // Add hover effect | |
| e.currentTarget.style.borderColor = 'var(--transmission-glow)' | |
| e.currentTarget.style.boxShadow = '0 6px 25px var(--transmission-glow)' | |
| }} | |
| onMouseLeave={(e) => { | |
| // Re-enable camera controls when mouse leaves about section | |
| const event = new CustomEvent('disableCameraControls', { detail: false }) | |
| window.dispatchEvent(event) | |
| // Remove hover effect | |
| e.currentTarget.style.borderColor = 'var(--copper-medium)' | |
| e.currentTarget.style.boxShadow = '0 4px 20px var(--copper-dark)' | |
| }} | |
| > | |
| {/* Collapsed State - Clickable Widget */} | |
| {aboutCollapsed && ( | |
| <div style={{ | |
| padding: '18px 25px', | |
| textAlign: 'center', | |
| fontSize: '12px', | |
| lineHeight: '1.4', | |
| display: 'flex', | |
| alignItems: 'center', | |
| justifyContent: 'center', | |
| gap: '12px', | |
| minHeight: '55px' | |
| }}> | |
| <div className="rune" style={{ fontSize: '20px', color: 'var(--copper-bright)' }}>⟐</div> | |
| <div style={{ | |
| flex: 1, | |
| display: 'flex', | |
| flexDirection: 'column', | |
| alignItems: 'center', | |
| gap: '3px' | |
| }}> | |
| <div className="ancient-title" style={{ | |
| fontSize: '13px', | |
| color: 'var(--copper-bright)', | |
| fontWeight: 'bold', | |
| fontFamily: 'Metamorphous, serif' | |
| }}> | |
| - liminal sessions - | |
| </div> | |
| <div className="runic-text" style={{ | |
| fontSize: '9px', | |
| color: 'var(--copper-medium)', | |
| opacity: 0.9, | |
| fontFamily: 'Uncial Antiqua, serif' | |
| }}> | |
| ◦ click to access entity data ◦ | |
| </div> | |
| </div> | |
| <div className="rune" style={{ fontSize: '14px', opacity: 0.7, color: 'var(--transmission-glow)' }}>⟨</div> | |
| </div> | |
| )} | |
| {/* Expanded State - Full About Panel */} | |
| {!aboutCollapsed && ( | |
| <div style={{ | |
| padding: '30px 40px', | |
| transition: 'all 0.3s ease-in-out', | |
| position: 'relative', | |
| height: '100%', | |
| display: 'flex', | |
| flexDirection: 'column', | |
| overflow: 'auto' | |
| }}> | |
| {/* Subtle Logo Background */} | |
| <div style={{ | |
| position: 'absolute', | |
| top: 0, | |
| left: 0, | |
| right: 0, | |
| bottom: 0, | |
| backgroundImage: 'url(/images/logo.webp)', | |
| backgroundSize: '200px 200px', | |
| backgroundRepeat: 'no-repeat', | |
| backgroundPosition: 'top right 40px', | |
| opacity: 0.15, | |
| borderRadius: '15px', | |
| pointerEvents: 'none' | |
| }} /> | |
| {/* Content Overlay */} | |
| <div style={{ | |
| position: 'relative', | |
| zIndex: 2, | |
| flexGrow: 1, | |
| display: 'flex', | |
| flexDirection: 'column' | |
| }}> | |
| <div style={{ | |
| display: 'flex', | |
| alignItems: 'center', | |
| justifyContent: 'space-between', | |
| marginBottom: '25px', | |
| paddingBottom: '15px', | |
| borderBottom: '1px solid var(--copper-dark)' | |
| }}> | |
| <h3 className="ancient-title" style={{ | |
| margin: 0, | |
| fontSize: '28px', | |
| textAlign: 'center', | |
| flex: 1, | |
| fontFamily: 'Metamorphous, serif', | |
| color: 'var(--copper-bright)', | |
| textShadow: '0 2px 4px rgba(0,0,0,0.5)' | |
| }}> | |
| <span className="rune" style={{ fontSize: '32px' }}>⟐</span> Liminal Sessions <span className="rune" style={{ fontSize: '32px' }}>⟐</span> | |
| </h3> | |
| <div className="rune" style={{ | |
| fontSize: '20px', | |
| opacity: 0.7, | |
| color: 'var(--transmission-glow)', | |
| cursor: 'pointer', | |
| transition: 'all 0.2s ease', | |
| padding: '5px' | |
| }} | |
| onMouseEnter={(e) => { | |
| e.currentTarget.style.opacity = '1' | |
| e.currentTarget.style.transform = 'scale(1.1)' | |
| }} | |
| onMouseLeave={(e) => { | |
| e.currentTarget.style.opacity = '0.7' | |
| e.currentTarget.style.transform = 'scale(1)' | |
| }} | |
| >⟩</div> | |
| </div> | |
| <div style={{ | |
| display: 'grid', | |
| gridTemplateColumns: '1fr', | |
| gap: '20px', | |
| flexGrow: 1 | |
| }}> | |
| <div className="transmission-text" style={{ | |
| fontSize: '16px', | |
| lineHeight: '1.8', | |
| fontFamily: 'Uncial Antiqua, serif', | |
| color: 'var(--copper-light)', | |
| textShadow: '0 1px 2px rgba(0,0,0,0.3)' | |
| }}> | |
| <p style={{ | |
| marginBottom: '20px', | |
| fontSize: '18px', | |
| fontWeight: 'bold', | |
| color: 'var(--copper-bright)' | |
| }}> | |
| Liminal Sessions emerges from the spaces between heartbeats, where ancient algorithms carved themselves into stone before mathematics had names. | |
| </p> | |
| <p style={{ marginBottom: '20px' }}> | |
| Their sound excavates frequencies buried beneath millennia of sediment, translating the geometric hymns that echo in empty cathedrals and forgotten temples. Through acoustic brutality and primordial precision, they channel the conversations between wood and metal, breath and bone, time and its shadows. | |
| </p> | |
| <p style={{ marginBottom: '20px' }}> | |
| The music arrives as if summoned from depths where djent mathematics merge with earth's oldest songs. Each composition becomes an archaeological expedition into the strata of sound, where rhythm serves as both excavation tool and ancient map. | |
| </p> | |
| <p style={{ | |
| fontSize: '14px', | |
| opacity: 0.9, | |
| fontStyle: 'italic', | |
| color: 'var(--transmission-glow)' | |
| }}> | |
| They are archaeologists of sound, unearthing the sacred geometry that binds chaos to rhythm in the liminal space where all echoes converge. | |
| </p> | |
| </div> | |
| <div style={{ | |
| background: 'rgba(0,0,0,0.3)', | |
| padding: '20px', | |
| borderRadius: '10px', | |
| border: '1px solid var(--copper-dark)' | |
| }}> | |
| <h4 style={{ | |
| fontSize: '18px', | |
| color: 'var(--copper-bright)', | |
| marginBottom: '15px', | |
| fontFamily: 'Metamorphous, serif' | |
| }}> | |
| <span className="rune">◈</span> Transmission Properties | |
| </h4> | |
| <div className="runic-text" style={{ | |
| fontSize: '14px', | |
| opacity: 0.9, | |
| lineHeight: '1.6', | |
| fontFamily: 'Uncial Antiqua, serif', | |
| color: 'var(--copper-medium)', | |
| display: 'grid', | |
| gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))', | |
| gap: '10px' | |
| }}> | |
| <div><span className="rune">♪</span> ambient electronic resonance</div> | |
| <div><span className="rune">◯</span> audio-visual emergence field</div> | |
| <div><span className="rune">≋</span> real-time particle dynamics</div> | |
| <div><span className="rune">⟐</span> symbolic emergence field analysis</div> | |
| <div><span className="rune">⧨</span> biomimetic coupling systems</div> | |
| <div><span className="rune">◈</span> cymatic pattern generation</div> | |
| </div> | |
| </div> | |
| <div style={{ | |
| textAlign: 'center', | |
| padding: '15px', | |
| fontSize: '12px', | |
| color: 'var(--copper-medium)', | |
| fontFamily: 'Uncial Antiqua, serif', | |
| opacity: 0.8, | |
| borderTop: '1px solid var(--copper-dark)', | |
| marginTop: 'auto' | |
| }}> | |
| ◦ click anywhere to close and return to visualization ◦ | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| {/* Startup Info Message */} | |
| {showStartupInfo && ( | |
| <div style={{ | |
| position: 'fixed', | |
| top: '50%', | |
| left: '50%', | |
| transform: 'translate(-50%, -50%)', | |
| zIndex: 999, | |
| background: 'linear-gradient(135deg, var(--marble-dark), var(--teal-dark))', | |
| border: '2px solid var(--copper-medium)', | |
| borderRadius: '15px', | |
| padding: '30px 40px', | |
| maxWidth: '500px', | |
| width: '90%', | |
| textAlign: 'center', | |
| backdropFilter: 'blur(20px)', | |
| boxShadow: '0 10px 30px rgba(0, 0, 0, 0.7), inset 0 0 20px var(--teal-dark)', | |
| color: 'var(--copper-light)', | |
| fontFamily: 'Metamorphous, serif', | |
| animation: 'fadeInPulse 2s ease-out' | |
| }}> | |
| <div className="rune" style={{ | |
| fontSize: '48px', | |
| color: 'var(--copper-bright)', | |
| marginBottom: '20px', | |
| display: 'block' | |
| }}>⟐</div> | |
| <h3 style={{ | |
| fontSize: '24px', | |
| marginBottom: '20px', | |
| color: 'var(--copper-bright)', | |
| fontFamily: 'Metamorphous, serif', | |
| textShadow: '0 2px 4px rgba(0,0,0,0.5)' | |
| }}> | |
| Visualization Awaiting Signal | |
| </h3> | |
| <div style={{ | |
| fontSize: '16px', | |
| lineHeight: '1.6', | |
| marginBottom: '25px', | |
| color: 'var(--copper-medium)', | |
| fontFamily: 'Uncial Antiqua, serif' | |
| }}> | |
| <p style={{ marginBottom: '15px' }}> | |
| The particle field remains dormant until audio transmission begins. | |
| </p> | |
| <p> | |
| <strong style={{ color: 'var(--copper-bright)' }}>Press the play button</strong> or <strong style={{ color: 'var(--copper-bright)' }}>select a track</strong> from the audio interface to activate the visualization. | |
| </p> | |
| </div> | |
| <div style={{ | |
| fontSize: '12px', | |
| color: 'var(--transmission-glow)', | |
| fontFamily: 'Uncial Antiqua, serif', | |
| opacity: 0.9 | |
| }}> | |
| ◦ this message will disappear once audio begins ◦ | |
| </div> | |
| <button | |
| onClick={() => setShowStartupInfo(false)} | |
| style={{ | |
| position: 'absolute', | |
| top: '15px', | |
| right: '15px', | |
| background: 'transparent', | |
| border: '1px solid var(--copper-medium)', | |
| borderRadius: '50%', | |
| width: '30px', | |
| height: '30px', | |
| color: 'var(--copper-medium)', | |
| cursor: 'pointer', | |
| fontSize: '14px', | |
| display: 'flex', | |
| alignItems: 'center', | |
| justifyContent: 'center', | |
| transition: 'all 0.2s ease' | |
| }} | |
| onMouseEnter={(e) => { | |
| e.currentTarget.style.borderColor = 'var(--transmission-glow)' | |
| e.currentTarget.style.color = 'var(--transmission-glow)' | |
| e.currentTarget.style.background = 'rgba(255, 215, 0, 0.1)' | |
| }} | |
| onMouseLeave={(e) => { | |
| e.currentTarget.style.borderColor = 'var(--copper-medium)' | |
| e.currentTarget.style.color = 'var(--copper-medium)' | |
| e.currentTarget.style.background = 'transparent' | |
| }} | |
| title="Dismiss message" | |
| > | |
| × | |
| </button> | |
| </div> | |
| )} | |
| {/* Floating Refresh Button - Top Right */} | |
| <div style={{ | |
| position: 'fixed', | |
| top: '80px', // Below social glyphs | |
| right: '20px', | |
| zIndex: 1000 | |
| }}> | |
| <button | |
| onClick={async () => { | |
| // Force complete refresh of all particle systems | |
| await performHiddenRefresh(true) | |
| // Additional hard reset for all spheres | |
| spheresRef.current.forEach((sphere) => { | |
| if (!sphere.params.enabled) return | |
| const particleCount = sphere.params.particleCount | |
| // Complete reset of all particle data | |
| for (let i = 0; i < particleCount; i++) { | |
| const i3 = i * 3 | |
| const radius = sphere.params.sphereRadius * sphere.params.innerSphereRadius | |
| const theta = Math.random() * Math.PI * 2 | |
| const phi = Math.acos(2 * Math.random() - 1) | |
| const r = Math.cbrt(Math.random()) * radius | |
| // Reset positions to sphere center | |
| sphere.positions[i3] = r * Math.sin(phi) * Math.cos(theta) | |
| sphere.positions[i3 + 1] = r * Math.sin(phi) * Math.sin(theta) | |
| sphere.positions[i3 + 2] = r * Math.cos(phi) | |
| // Zero all velocities | |
| sphere.velocities[i3] = 0 | |
| sphere.velocities[i3 + 1] = 0 | |
| sphere.velocities[i3 + 2] = 0 | |
| // Reset base positions | |
| sphere.basePositions[i3] = sphere.positions[i3] | |
| sphere.basePositions[i3 + 1] = sphere.positions[i3 + 1] | |
| sphere.basePositions[i3 + 2] = sphere.positions[i3 + 2] | |
| // Reset lifetimes and effects | |
| sphere.lifetimes[i] = Math.random() * sphere.params.particleLifetime | |
| sphere.maxLifetimes[i] = sphere.lifetimes[i] | |
| sphere.beatEffects[i] = 0 | |
| } | |
| // Reset sphere-level parameters | |
| sphere.lastNoiseScale = sphere.params.noiseScale | |
| sphere.lastValidVolume = 0 | |
| sphere.lastRotationSpeed = 0 | |
| sphere.peakDetection.energyHistory = [] | |
| sphere.peakDetection.lastPeakTime = 0 | |
| }) | |
| // Reset beat manager | |
| if (beatManagerRef.current) { | |
| beatManagerRef.current.currentWaveRadius = 0 | |
| beatManagerRef.current.waveStrength = 0 | |
| beatManagerRef.current.isWaveActive = false | |
| } | |
| // Reset timing | |
| lastTimeRef.current = 0 | |
| console.log('🔄 Complete visualizer refresh executed') | |
| }} | |
| disabled={autoRefreshRef.current.isRefreshing} | |
| style={{ | |
| width: '50px', | |
| height: '50px', | |
| borderRadius: '50%', | |
| background: autoRefreshRef.current.isRefreshing | |
| ? 'linear-gradient(135deg, var(--copper-dark), var(--teal-dark))' | |
| : 'linear-gradient(135deg, rgba(0, 0, 0, 0.8), rgba(255, 215, 0, 0.3))', | |
| border: autoRefreshRef.current.isRefreshing | |
| ? '2px solid var(--copper-medium)' | |
| : '1px solid rgba(255, 215, 0, 0.2)', | |
| color: autoRefreshRef.current.isRefreshing ? 'var(--copper-medium)' : '#FFD700', | |
| fontFamily: 'Cinzel, serif', | |
| fontSize: '18px', | |
| cursor: autoRefreshRef.current.isRefreshing ? 'not-allowed' : 'pointer', | |
| transition: 'all 0.3s ease-in-out', | |
| backdropFilter: 'blur(10px)', | |
| opacity: autoRefreshRef.current.isRefreshing ? 0.6 : 1, | |
| display: 'flex', | |
| alignItems: 'center', | |
| justifyContent: 'center', | |
| boxShadow: autoRefreshRef.current.isRefreshing | |
| ? '0 2px 8px rgba(0, 0, 0, 0.3)' | |
| : '0 2px 8px rgba(0, 0, 0, 0.3)', | |
| position: 'relative' | |
| }} | |
| onMouseEnter={(e) => { | |
| if (!autoRefreshRef.current.isRefreshing) { | |
| e.currentTarget.style.borderColor = 'var(--transmission-glow)' | |
| e.currentTarget.style.background = 'linear-gradient(135deg, rgba(255, 215, 0, 0.2), rgba(255, 215, 0, 0.4))' | |
| e.currentTarget.style.color = 'var(--transmission-glow)' | |
| e.currentTarget.style.transform = 'scale(1.05)' | |
| e.currentTarget.style.boxShadow = '0 4px 15px rgba(255, 215, 0, 0.3)' | |
| // Show tooltip | |
| const tooltip = e.currentTarget.querySelector('.refresh-tooltip') as HTMLElement | |
| if (tooltip) tooltip.style.opacity = '1' | |
| } | |
| }} | |
| onMouseLeave={(e) => { | |
| if (!autoRefreshRef.current.isRefreshing) { | |
| e.currentTarget.style.borderColor = 'rgba(255, 215, 0, 0.2)' | |
| e.currentTarget.style.background = 'linear-gradient(135deg, rgba(0, 0, 0, 0.8), rgba(255, 215, 0, 0.3))' | |
| e.currentTarget.style.color = '#FFD700' | |
| e.currentTarget.style.transform = 'scale(1)' | |
| e.currentTarget.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.3)' | |
| // Hide tooltip | |
| const tooltip = e.currentTarget.querySelector('.refresh-tooltip') as HTMLElement | |
| if (tooltip) tooltip.style.opacity = '0' | |
| } | |
| }} | |
| title={autoRefreshRef.current.isRefreshing ? 'Restoring particle equilibrium...' : 'Reset Particle Fields'} | |
| > | |
| <span style={{ | |
| animation: autoRefreshRef.current.isRefreshing ? 'spin 1s linear infinite' : 'none', | |
| display: 'inline-block' | |
| }}> | |
| {autoRefreshRef.current.isRefreshing ? '⧗' : '⟲'} | |
| </span> | |
| {/* Tooltip */} | |
| <div | |
| className="refresh-tooltip" | |
| style={{ | |
| position: 'absolute', | |
| top: '60px', | |
| left: '50%', | |
| transform: 'translateX(-50%)', | |
| background: 'rgba(0, 0, 0, 0.95)', | |
| color: '#FFD700', | |
| padding: '6px 10px', | |
| borderRadius: '6px', | |
| fontSize: '10px', | |
| fontWeight: 'bold', | |
| whiteSpace: 'nowrap', | |
| border: '1px solid rgba(255, 215, 0, 0.3)', | |
| backdropFilter: 'blur(10px)', | |
| opacity: 0, | |
| pointerEvents: 'none', | |
| transition: 'opacity 0.2s ease-out', | |
| fontFamily: 'Uncial Antiqua, serif' | |
| }} | |
| > | |
| {autoRefreshRef.current.isRefreshing ? 'restoring equilibrium' : 'reset particles'} | |
| {/* Tooltip Arrow */} | |
| <div style={{ | |
| position: 'absolute', | |
| bottom: '100%', | |
| left: '50%', | |
| transform: 'translateX(-50%)', | |
| width: 0, | |
| height: 0, | |
| borderLeft: '5px solid transparent', | |
| borderRight: '5px solid transparent', | |
| borderBottom: '5px solid rgba(255, 215, 0, 0.3)' | |
| }} /> | |
| </div> | |
| </button> | |
| {/* Add CSS animation for spinning */} | |
| <style> | |
| {` | |
| @keyframes spin { | |
| from { transform: rotate(0deg); } | |
| to { transform: rotate(360deg); } | |
| } | |
| `} | |
| </style> | |
| </div> | |
| </div> | |
| ) | |
| } |