Spaces:
Sleeping
Sleeping
Upload 32 files
Browse files- .gitattributes +9 -0
- config.json +492 -0
- index.html +130 -0
- main.js +1531 -0
- package.json +32 -0
- public/songs/AntlerKing2.mp3 +3 -0
- public/songs/Are_You_One_Of_Us.mp3 +3 -0
- public/songs/Barren_Sky.mp3 +3 -0
- public/songs/Burn_For_You.mp3 +3 -0
- public/songs/Home.mp3 +3 -0
- public/songs/Maybe.mp3 +3 -0
- public/songs/Mountain.mp3 +3 -0
- public/songs/Senua.mp3 +3 -0
- public/songs/Unraveled.mp3 +3 -0
- src/.DS_Store +0 -0
- src/components/ConfigPanel.tsx +403 -0
- src/components/ControlPlinth.tsx +681 -0
- src/components/LivingNexusApp.tsx +293 -0
- src/components/SocialGlyphs.tsx +175 -0
- src/components/WorldTreeVisualizer.tsx +2394 -0
- src/engine/AudioEngine.ts +665 -0
- src/main.tsx +7 -0
- src/store/nexusStore.ts +190 -0
- src/styles/global.css +965 -0
- src/types/audio.ts +64 -0
- src/types/visualization.ts +199 -0
- src/utils/configLoader.ts +632 -0
- src/utils/entronaut.ts +503 -0
- src/utils/noise.ts +188 -0
- src/utils/performanceOptimizer.ts +347 -0
- tsconfig.json +25 -0
- tsconfig.node.json +10 -0
- vite.config.ts +67 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,12 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
public/songs/AntlerKing2.mp3 filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
public/songs/Are_You_One_Of_Us.mp3 filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
public/songs/Barren_Sky.mp3 filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
public/songs/Burn_For_You.mp3 filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
public/songs/Home.mp3 filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
public/songs/Maybe.mp3 filter=lfs diff=lfs merge=lfs -text
|
| 42 |
+
public/songs/Mountain.mp3 filter=lfs diff=lfs merge=lfs -text
|
| 43 |
+
public/songs/Senua.mp3 filter=lfs diff=lfs merge=lfs -text
|
| 44 |
+
public/songs/Unraveled.mp3 filter=lfs diff=lfs merge=lfs -text
|
config.json
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"visualization": {
|
| 3 |
+
"general": {
|
| 4 |
+
"maxSpheres": 5,
|
| 5 |
+
"defaultActiveSpheres": 5,
|
| 6 |
+
"performanceMode": false,
|
| 7 |
+
"debugLogging": false,
|
| 8 |
+
"debugLogFrequency": 0.01
|
| 9 |
+
},
|
| 10 |
+
"particles": {
|
| 11 |
+
"defaultParticleCount": 6000,
|
| 12 |
+
"minParticleCount": 2000,
|
| 13 |
+
"maxParticleCount": 8000,
|
| 14 |
+
"particleSize": 0.9,
|
| 15 |
+
"particleLifetime": 12.0,
|
| 16 |
+
"particleSizeVariation": 0.15,
|
| 17 |
+
"particleOpacity": 0.8,
|
| 18 |
+
"organicGlow": 0.7,
|
| 19 |
+
"naturalScattering": 0.25
|
| 20 |
+
},
|
| 21 |
+
"spheres": {
|
| 22 |
+
"baseRadius": 1.0,
|
| 23 |
+
"radiusIncrement": 0.08,
|
| 24 |
+
"innerSphereRadius": 0.7,
|
| 25 |
+
"maxSphereRadius": 2.0,
|
| 26 |
+
"containmentPullback": 0.12,
|
| 27 |
+
"overflowDamping": 0.92
|
| 28 |
+
},
|
| 29 |
+
"physics": {
|
| 30 |
+
"turbulenceStrength": 0.004,
|
| 31 |
+
"noiseScale": 2.0,
|
| 32 |
+
"noiseScaleVariation": 0.25,
|
| 33 |
+
"noiseSpeed": 0.25,
|
| 34 |
+
"defaultDamping": 0.97,
|
| 35 |
+
"velocityDecay": 0.98,
|
| 36 |
+
"beatForceMultiplier": 0.0006,
|
| 37 |
+
"maxVelocity": 0.06,
|
| 38 |
+
"stabilityThreshold": 6.0,
|
| 39 |
+
"emergencyDamping": 0.2,
|
| 40 |
+
"organicFlowFactor": 0.9,
|
| 41 |
+
"naturalBreathing": 0.35
|
| 42 |
+
},
|
| 43 |
+
"rotation": {
|
| 44 |
+
"rotationSpeedMin": 0.001,
|
| 45 |
+
"rotationSpeedMax": 0.05,
|
| 46 |
+
"rotationSpeedIncrement": 0.006,
|
| 47 |
+
"rotationSmoothness": 0.12,
|
| 48 |
+
"autoRotateSpeed": 0.4
|
| 49 |
+
}
|
| 50 |
+
},
|
| 51 |
+
"audio": {
|
| 52 |
+
"frequencyRanges": [
|
| 53 |
+
{
|
| 54 |
+
"name": "sub-bass",
|
| 55 |
+
"minFrequency": 20,
|
| 56 |
+
"maxFrequency": 80,
|
| 57 |
+
"beatThreshold": 0.6,
|
| 58 |
+
"volumeChangeThreshold": 0.05
|
| 59 |
+
},
|
| 60 |
+
{
|
| 61 |
+
"name": "bass",
|
| 62 |
+
"minFrequency": 120,
|
| 63 |
+
"maxFrequency": 250,
|
| 64 |
+
"beatThreshold": 0.65,
|
| 65 |
+
"volumeChangeThreshold": 0.08
|
| 66 |
+
},
|
| 67 |
+
{
|
| 68 |
+
"name": "mid",
|
| 69 |
+
"minFrequency": 250,
|
| 70 |
+
"maxFrequency": 800,
|
| 71 |
+
"beatThreshold": 0.7,
|
| 72 |
+
"volumeChangeThreshold": 0.1
|
| 73 |
+
},
|
| 74 |
+
{
|
| 75 |
+
"name": "high-mid",
|
| 76 |
+
"minFrequency": 1000,
|
| 77 |
+
"maxFrequency": 4000,
|
| 78 |
+
"beatThreshold": 0.75,
|
| 79 |
+
"volumeChangeThreshold": 0.12
|
| 80 |
+
},
|
| 81 |
+
{
|
| 82 |
+
"name": "high",
|
| 83 |
+
"minFrequency": 5000,
|
| 84 |
+
"maxFrequency": 10000,
|
| 85 |
+
"beatThreshold": 0.8,
|
| 86 |
+
"volumeChangeThreshold": 0.15
|
| 87 |
+
}
|
| 88 |
+
],
|
| 89 |
+
"beatDetection": {
|
| 90 |
+
"energyHistoryLength": 25,
|
| 91 |
+
"minTimeBetweenPeaks": 180,
|
| 92 |
+
"beatStrength": 0.018,
|
| 93 |
+
"waveDecayRate": 0.98,
|
| 94 |
+
"peakThresholdMultiplier": 1.25
|
| 95 |
+
},
|
| 96 |
+
"volumeSmoothing": {
|
| 97 |
+
"enabled": true,
|
| 98 |
+
"smoothingFactor": 0.12,
|
| 99 |
+
"changeThresholdDefault": 0.08
|
| 100 |
+
}
|
| 101 |
+
},
|
| 102 |
+
"entronaut": {
|
| 103 |
+
"enabled": true,
|
| 104 |
+
"sefaAnalysis": {
|
| 105 |
+
"fieldSize": 256,
|
| 106 |
+
"fieldWidth": 16,
|
| 107 |
+
"fieldHeight": 16,
|
| 108 |
+
"updateFrequency": 5,
|
| 109 |
+
"temporalSmoothing": 0.92,
|
| 110 |
+
"intensitySmoothing": 0.08
|
| 111 |
+
},
|
| 112 |
+
"weights": {
|
| 113 |
+
"alpha_amplitude": 1.0,
|
| 114 |
+
"alpha_phase": 0.7,
|
| 115 |
+
"alpha_entropy": 1.2,
|
| 116 |
+
"alpha_curvature": 0.8,
|
| 117 |
+
"alpha_spectral": 0.9
|
| 118 |
+
},
|
| 119 |
+
"adaptiveCoupling": {
|
| 120 |
+
"enabled": true,
|
| 121 |
+
"updateFrequency": 6,
|
| 122 |
+
"dampingBase": 0.92,
|
| 123 |
+
"dampingVariation": 0.25,
|
| 124 |
+
"diffusionBase": 0.008,
|
| 125 |
+
"diffusionVariation": 0.5,
|
| 126 |
+
"couplingBase": 0.015,
|
| 127 |
+
"couplingVariation": 0.03,
|
| 128 |
+
"flockingRadius": 0.2,
|
| 129 |
+
"flockingInfluence": 0.15,
|
| 130 |
+
"metabolicPulseStrength": 0.4,
|
| 131 |
+
"neighborCheckStep": 8,
|
| 132 |
+
"neighborRange": 15
|
| 133 |
+
},
|
| 134 |
+
"biomimeticBehavior": {
|
| 135 |
+
"collectiveMemory": 0.85,
|
| 136 |
+
"emergenceThreshold": 0.4,
|
| 137 |
+
"coherenceWeight": 0.8,
|
| 138 |
+
"complexityWeight": 0.8,
|
| 139 |
+
"networkInfluence": 0.15,
|
| 140 |
+
"syncPulseStrength": 0.15
|
| 141 |
+
}
|
| 142 |
+
},
|
| 143 |
+
"tendrils": {
|
| 144 |
+
"enabled": true,
|
| 145 |
+
"maxTendrils": 6000,
|
| 146 |
+
"defaultDensity": 0.25,
|
| 147 |
+
"maxTendrilLength": 0.7,
|
| 148 |
+
"tendrilSegments": 6,
|
| 149 |
+
"updateInterval": 120,
|
| 150 |
+
"sampleParticlesPerSphere": 8,
|
| 151 |
+
"particleSampleStep": 1200,
|
| 152 |
+
"curvature": {
|
| 153 |
+
"midPointMultiplier": 0.18,
|
| 154 |
+
"undulationStrength": 0.04,
|
| 155 |
+
"timeFactorBase": 0.0008,
|
| 156 |
+
"timeFactorCurve": 0.0012,
|
| 157 |
+
"spatialFrequency": 8
|
| 158 |
+
},
|
| 159 |
+
"visual": {
|
| 160 |
+
"baseOpacity": 0.25,
|
| 161 |
+
"glowIntensity": 0.8,
|
| 162 |
+
"colorIntensityBase": 0.25,
|
| 163 |
+
"colorIntensityVariation": 0.6,
|
| 164 |
+
"blendingMode": "additive"
|
| 165 |
+
}
|
| 166 |
+
},
|
| 167 |
+
"cymatics": {
|
| 168 |
+
"enabled": false,
|
| 169 |
+
"intensity": 0.0015,
|
| 170 |
+
"patterns": {
|
| 171 |
+
"radialWaves": {
|
| 172 |
+
"frequencyFactor": 0.08,
|
| 173 |
+
"petalCount": 5,
|
| 174 |
+
"amplitudeZ": 0.4
|
| 175 |
+
},
|
| 176 |
+
"sphericalHarmonics": {
|
| 177 |
+
"lMultiplier": 0.4,
|
| 178 |
+
"mMultiplier": 0.25,
|
| 179 |
+
"timeMultiplier": 1.5
|
| 180 |
+
},
|
| 181 |
+
"geometricLattice": {
|
| 182 |
+
"frequencyFactor": 0.15
|
| 183 |
+
},
|
| 184 |
+
"flowerOfLife": {
|
| 185 |
+
"frequencyFactor": 0.12,
|
| 186 |
+
"basePetals": 5,
|
| 187 |
+
"petalVariation": 0.08,
|
| 188 |
+
"amplitudeZ": 0.25
|
| 189 |
+
}
|
| 190 |
+
},
|
| 191 |
+
"breathing": {
|
| 192 |
+
"enabled": true,
|
| 193 |
+
"speed": 0.4,
|
| 194 |
+
"intensityBase": 0.25,
|
| 195 |
+
"intensityVariation": 0.6
|
| 196 |
+
}
|
| 197 |
+
},
|
| 198 |
+
"dysonSphere": {
|
| 199 |
+
"enabled": false,
|
| 200 |
+
"vines": {
|
| 201 |
+
"maxVines": 18,
|
| 202 |
+
"maxSegmentsPerVine": 24,
|
| 203 |
+
"sphereRadius": 2.6,
|
| 204 |
+
"growthSpeed": 0.015,
|
| 205 |
+
"vineThickness": 0.006,
|
| 206 |
+
"branchingProbability": 0.12,
|
| 207 |
+
"maxBranches": 2,
|
| 208 |
+
"curvature": 0.25,
|
| 209 |
+
"audioReactivity": 1.0,
|
| 210 |
+
"organicVariation": 0.35
|
| 211 |
+
},
|
| 212 |
+
"nodes": {
|
| 213 |
+
"nodeCount": 36,
|
| 214 |
+
"nodeSize": 0.018,
|
| 215 |
+
"pulseIntensity": 1.2,
|
| 216 |
+
"connectionThreshold": 0.7,
|
| 217 |
+
"energyFlow": 0.5
|
| 218 |
+
},
|
| 219 |
+
"growth": {
|
| 220 |
+
"seedPoints": 6,
|
| 221 |
+
"growthCycles": 50,
|
| 222 |
+
"maturityTime": 40.0,
|
| 223 |
+
"renewalRate": 0.06,
|
| 224 |
+
"emergenceThreshold": 0.25
|
| 225 |
+
},
|
| 226 |
+
"visual": {
|
| 227 |
+
"bloomIntensity": 1.5,
|
| 228 |
+
"wireframeOpacity": 0.5,
|
| 229 |
+
"nodeGlow": 1.8,
|
| 230 |
+
"vineGlow": 1.2,
|
| 231 |
+
"pulsationSpeed": 0.7
|
| 232 |
+
}
|
| 233 |
+
},
|
| 234 |
+
"colors": {
|
| 235 |
+
"sphereColorSchemes": [
|
| 236 |
+
{
|
| 237 |
+
"name": "sub-bass",
|
| 238 |
+
"start": "#2d5016",
|
| 239 |
+
"end": "#7d4f39"
|
| 240 |
+
},
|
| 241 |
+
{
|
| 242 |
+
"name": "bass",
|
| 243 |
+
"start": "#8b5a2b",
|
| 244 |
+
"end": "#4a6741"
|
| 245 |
+
},
|
| 246 |
+
{
|
| 247 |
+
"name": "mid",
|
| 248 |
+
"start": "#cd853f",
|
| 249 |
+
"end": "#556b2f"
|
| 250 |
+
},
|
| 251 |
+
{
|
| 252 |
+
"name": "high-mid",
|
| 253 |
+
"start": "#daa520",
|
| 254 |
+
"end": "#2e8b57"
|
| 255 |
+
},
|
| 256 |
+
{
|
| 257 |
+
"name": "high",
|
| 258 |
+
"start": "#b8860b",
|
| 259 |
+
"end": "#20b2aa"
|
| 260 |
+
}
|
| 261 |
+
],
|
| 262 |
+
"organicPalette": {
|
| 263 |
+
"deepForest": "#2d5016",
|
| 264 |
+
"ancientBronze": "#cd7f32",
|
| 265 |
+
"copperGlow": "#b87333",
|
| 266 |
+
"goldLeaf": "#daa520",
|
| 267 |
+
"earthBrown": "#8b4513",
|
| 268 |
+
"stoneBark": "#696969",
|
| 269 |
+
"mossGreen": "#556b2f",
|
| 270 |
+
"tealDepth": "#20b2aa",
|
| 271 |
+
"amberWarm": "#ffbf00",
|
| 272 |
+
"ironOxide": "#7d4f39",
|
| 273 |
+
"forestShadow": "#1e3a0f",
|
| 274 |
+
"vineGreen": "#4a6741",
|
| 275 |
+
"copperPatina": "#87ceeb",
|
| 276 |
+
"burnishedGold": "#b8860b",
|
| 277 |
+
"darkTeal": "#2f4f4f"
|
| 278 |
+
},
|
| 279 |
+
"dysonVines": {
|
| 280 |
+
"primaryVine": "#4a6741",
|
| 281 |
+
"secondaryVine": "#556b2f",
|
| 282 |
+
"vineHighlight": "#7d8471",
|
| 283 |
+
"nodeColor": "#cd7f32",
|
| 284 |
+
"connectionColor": "#b87333",
|
| 285 |
+
"growthTip": "#daa520",
|
| 286 |
+
"anchorPoints": "#2d5016"
|
| 287 |
+
},
|
| 288 |
+
"tendrilColors": {
|
| 289 |
+
"baseColor": [0.4, 0.3, 0.2],
|
| 290 |
+
"frequencyColorMap": {
|
| 291 |
+
"lowFreq": [0.8, 0.5, 0.3],
|
| 292 |
+
"midFreq": [0.6, 0.7, 0.3],
|
| 293 |
+
"highFreq": [0.3, 0.6, 0.6]
|
| 294 |
+
},
|
| 295 |
+
"organicBlend": {
|
| 296 |
+
"forest": [0.18, 0.31, 0.09],
|
| 297 |
+
"bronze": [0.80, 0.50, 0.20],
|
| 298 |
+
"copper": [0.72, 0.45, 0.20],
|
| 299 |
+
"gold": [0.85, 0.65, 0.13],
|
| 300 |
+
"teal": [0.13, 0.70, 0.67]
|
| 301 |
+
}
|
| 302 |
+
}
|
| 303 |
+
},
|
| 304 |
+
"camera": {
|
| 305 |
+
"defaultPosition": [0, 0, 2.5],
|
| 306 |
+
"fov": 75,
|
| 307 |
+
"near": 0.1,
|
| 308 |
+
"far": 1000,
|
| 309 |
+
"controls": {
|
| 310 |
+
"enableDamping": true,
|
| 311 |
+
"dampingFactor": 0.05,
|
| 312 |
+
"maxDistance": 50,
|
| 313 |
+
"minDistance": 0.1,
|
| 314 |
+
"maxPolarAngle": 3.14159,
|
| 315 |
+
"autoRotateSpeed": 0.4
|
| 316 |
+
},
|
| 317 |
+
"presets": {
|
| 318 |
+
"default": [0, 0, 2.5],
|
| 319 |
+
"inside": [0, 0, 0.5],
|
| 320 |
+
"far": [0, 0, 8],
|
| 321 |
+
"top": [0, 5, 0],
|
| 322 |
+
"side": [5, 0, 0]
|
| 323 |
+
}
|
| 324 |
+
},
|
| 325 |
+
"fog": {
|
| 326 |
+
"enabled": false,
|
| 327 |
+
"color": "#000000",
|
| 328 |
+
"near": 2.7,
|
| 329 |
+
"far": 3.7,
|
| 330 |
+
"density": 0.1
|
| 331 |
+
},
|
| 332 |
+
"rendering": {
|
| 333 |
+
"antialias": true,
|
| 334 |
+
"background": "#0F1B13",
|
| 335 |
+
"particleBlending": "additive",
|
| 336 |
+
"targetFPS": 60,
|
| 337 |
+
"adaptiveQuality": {
|
| 338 |
+
"enabled": true,
|
| 339 |
+
"minFPS": 35,
|
| 340 |
+
"qualityReductionStep": 0.08,
|
| 341 |
+
"qualityRecoveryDelay": 1500
|
| 342 |
+
},
|
| 343 |
+
"organicRendering": {
|
| 344 |
+
"warmthFactor": 1.1,
|
| 345 |
+
"naturalContrast": 0.9,
|
| 346 |
+
"earthyTones": true
|
| 347 |
+
}
|
| 348 |
+
},
|
| 349 |
+
"ui": {
|
| 350 |
+
"controls": {
|
| 351 |
+
"defaultCollapsed": true,
|
| 352 |
+
"position": "bottom-right",
|
| 353 |
+
"maxWidth": "280px",
|
| 354 |
+
"maxHeight": "70vh",
|
| 355 |
+
"opacity": 0.85
|
| 356 |
+
},
|
| 357 |
+
"about": {
|
| 358 |
+
"defaultCollapsed": true,
|
| 359 |
+
"position": "bottom-center",
|
| 360 |
+
"width": "350px"
|
| 361 |
+
},
|
| 362 |
+
"animation": {
|
| 363 |
+
"transitionDuration": "0.3s",
|
| 364 |
+
"hoverScale": 1.1,
|
| 365 |
+
"backdropBlur": "10px"
|
| 366 |
+
}
|
| 367 |
+
},
|
| 368 |
+
"performance": {
|
| 369 |
+
"adaptiveSettings": {
|
| 370 |
+
"enabled": true,
|
| 371 |
+
"thresholds": {
|
| 372 |
+
"particleReduction": 40,
|
| 373 |
+
"tendrilReduction": 35,
|
| 374 |
+
"sefaDisable": 30,
|
| 375 |
+
"couplingDisable": 25
|
| 376 |
+
}
|
| 377 |
+
},
|
| 378 |
+
"optimizations": {
|
| 379 |
+
"particleUpdateStep": 1,
|
| 380 |
+
"tendrilUpdateStep": 8,
|
| 381 |
+
"sefaUpdateStep": 15,
|
| 382 |
+
"couplingUpdateStep": 10,
|
| 383 |
+
"neighborCheckReduction": 10,
|
| 384 |
+
"maxCacheSize": 30,
|
| 385 |
+
"cacheLifetime": 400,
|
| 386 |
+
"bufferPoolSize": 3,
|
| 387 |
+
"adaptiveFrameSkipping": false,
|
| 388 |
+
"lodEnabled": false,
|
| 389 |
+
"memoryCleanupInterval": 8000
|
| 390 |
+
},
|
| 391 |
+
"huggingFaceOptimized": {
|
| 392 |
+
"enabled": true,
|
| 393 |
+
"maxParticlesPerSphere": 5000,
|
| 394 |
+
"minParticlesPerSphere": 2000,
|
| 395 |
+
"reducedComplexityMode": true,
|
| 396 |
+
"aggressiveCaching": false,
|
| 397 |
+
"cpuOptimized": true
|
| 398 |
+
}
|
| 399 |
+
},
|
| 400 |
+
"presets": {
|
| 401 |
+
"performance": {
|
| 402 |
+
"visualization": {
|
| 403 |
+
"particles": {
|
| 404 |
+
"defaultParticleCount": 4000
|
| 405 |
+
},
|
| 406 |
+
"physics": {
|
| 407 |
+
"turbulenceStrength": 0.003,
|
| 408 |
+
"maxVelocity": 0.04,
|
| 409 |
+
"stabilityThreshold": 4.0
|
| 410 |
+
}
|
| 411 |
+
},
|
| 412 |
+
"entronaut": {
|
| 413 |
+
"enabled": false
|
| 414 |
+
},
|
| 415 |
+
"tendrils": {
|
| 416 |
+
"enabled": false
|
| 417 |
+
},
|
| 418 |
+
"cymatics": {
|
| 419 |
+
"enabled": false
|
| 420 |
+
}
|
| 421 |
+
},
|
| 422 |
+
"extreme": {
|
| 423 |
+
"visualization": {
|
| 424 |
+
"general": {
|
| 425 |
+
"performanceMode": true
|
| 426 |
+
},
|
| 427 |
+
"particles": {
|
| 428 |
+
"defaultParticleCount": 80000
|
| 429 |
+
},
|
| 430 |
+
"physics": {
|
| 431 |
+
"turbulenceStrength": 0.001,
|
| 432 |
+
"maxVelocity": 0.02,
|
| 433 |
+
"stabilityThreshold": 2.0,
|
| 434 |
+
"emergencyDamping": 0.02
|
| 435 |
+
}
|
| 436 |
+
},
|
| 437 |
+
"entronaut": {
|
| 438 |
+
"enabled": false
|
| 439 |
+
},
|
| 440 |
+
"tendrils": {
|
| 441 |
+
"enabled": false,
|
| 442 |
+
"maxTendrils": 300
|
| 443 |
+
},
|
| 444 |
+
"cymatics": {
|
| 445 |
+
"enabled": false
|
| 446 |
+
},
|
| 447 |
+
"performance": {
|
| 448 |
+
"adaptiveSettings": {
|
| 449 |
+
"enabled": true,
|
| 450 |
+
"thresholds": {
|
| 451 |
+
"particleReduction": 40,
|
| 452 |
+
"tendrilReduction": 35,
|
| 453 |
+
"sefaDisable": 30,
|
| 454 |
+
"couplingDisable": 25
|
| 455 |
+
}
|
| 456 |
+
}
|
| 457 |
+
}
|
| 458 |
+
},
|
| 459 |
+
"quality": {
|
| 460 |
+
"particles": {
|
| 461 |
+
"defaultParticleCount": 20000,
|
| 462 |
+
"turbulenceStrength": 0.008
|
| 463 |
+
},
|
| 464 |
+
"entronaut": {
|
| 465 |
+
"enabled": true,
|
| 466 |
+
"sefaAnalysis": {
|
| 467 |
+
"fieldSize": 1024
|
| 468 |
+
}
|
| 469 |
+
},
|
| 470 |
+
"tendrils": {
|
| 471 |
+
"maxTendrils": 2000,
|
| 472 |
+
"tendrilSegments": 10
|
| 473 |
+
}
|
| 474 |
+
},
|
| 475 |
+
"minimal": {
|
| 476 |
+
"visualization": {
|
| 477 |
+
"general": {
|
| 478 |
+
"defaultActiveSpheres": 2
|
| 479 |
+
}
|
| 480 |
+
},
|
| 481 |
+
"particles": {
|
| 482 |
+
"defaultParticleCount": 2500
|
| 483 |
+
},
|
| 484 |
+
"entronaut": {
|
| 485 |
+
"enabled": false
|
| 486 |
+
},
|
| 487 |
+
"tendrils": {
|
| 488 |
+
"enabled": false
|
| 489 |
+
}
|
| 490 |
+
}
|
| 491 |
+
}
|
| 492 |
+
}
|
index.html
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Liminal Sessions - Interdimensional Transmission</title>
|
| 7 |
+
<meta name="description" content="Intercepted transmission from distant coordinates - biomimetic audio-visual emergence fields from the Liminal Sessions entity.">
|
| 8 |
+
|
| 9 |
+
<!-- Preconnect to Google Fonts -->
|
| 10 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 11 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 12 |
+
|
| 13 |
+
<!-- Ancient Transmission Fonts -->
|
| 14 |
+
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600;700&family=Uncial+Antiqua&family=Metamorphous&display=swap" rel="stylesheet">
|
| 15 |
+
|
| 16 |
+
<!-- Favicon and Meta -->
|
| 17 |
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
| 18 |
+
<meta name="theme-color" content="#FFD700">
|
| 19 |
+
|
| 20 |
+
<!-- OpenGraph / Social Media Meta -->
|
| 21 |
+
<meta property="og:title" content="Liminal Sessions - Interdimensional Transmission">
|
| 22 |
+
<meta property="og:description" content="Intercepted biomimetic audio-visual emergence fields from distant coordinates">
|
| 23 |
+
<meta property="og:type" content="website">
|
| 24 |
+
<meta property="og:image" content="/og-image.png">
|
| 25 |
+
|
| 26 |
+
<!-- Twitter Meta -->
|
| 27 |
+
<meta name="twitter:card" content="summary_large_image">
|
| 28 |
+
<meta name="twitter:title" content="Liminal Sessions">
|
| 29 |
+
<meta name="twitter:description" content="◦ Interdimensional transmission interface ◦">
|
| 30 |
+
|
| 31 |
+
<style>
|
| 32 |
+
/* Critical CSS for loading */
|
| 33 |
+
* {
|
| 34 |
+
margin: 0;
|
| 35 |
+
padding: 0;
|
| 36 |
+
box-sizing: border-box;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
html, body {
|
| 40 |
+
height: 100vh;
|
| 41 |
+
width: 100vw;
|
| 42 |
+
overflow: hidden;
|
| 43 |
+
font-family: 'Cinzel', 'Times New Roman', serif;
|
| 44 |
+
background: #0F1B13;
|
| 45 |
+
color: #A67B5B;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
#root {
|
| 49 |
+
height: 100vh;
|
| 50 |
+
width: 100vw;
|
| 51 |
+
position: relative;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
/* Loading screen */
|
| 55 |
+
.loading-screen {
|
| 56 |
+
position: fixed;
|
| 57 |
+
top: 0;
|
| 58 |
+
left: 0;
|
| 59 |
+
width: 100%;
|
| 60 |
+
height: 100%;
|
| 61 |
+
background: #0F1B13;
|
| 62 |
+
display: flex;
|
| 63 |
+
align-items: center;
|
| 64 |
+
justify-content: center;
|
| 65 |
+
z-index: 9999;
|
| 66 |
+
transition: opacity 0.5s ease;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
.loading-text {
|
| 70 |
+
font-family: 'Metamorphous', serif;
|
| 71 |
+
font-size: 20px;
|
| 72 |
+
color: #A67B5B;
|
| 73 |
+
text-align: center;
|
| 74 |
+
animation: organicPulse 3s ease-in-out infinite;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
@keyframes organicPulse {
|
| 78 |
+
0%, 100% {
|
| 79 |
+
opacity: 0.8;
|
| 80 |
+
transform: scale(1) rotate(0deg);
|
| 81 |
+
}
|
| 82 |
+
25% {
|
| 83 |
+
opacity: 0.9;
|
| 84 |
+
transform: scale(1.01) rotate(0.2deg);
|
| 85 |
+
}
|
| 86 |
+
50% {
|
| 87 |
+
opacity: 1;
|
| 88 |
+
transform: scale(1.02) rotate(0deg);
|
| 89 |
+
}
|
| 90 |
+
75% {
|
| 91 |
+
opacity: 0.85;
|
| 92 |
+
transform: scale(1.01) rotate(-0.2deg);
|
| 93 |
+
}
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
.loading-screen.hidden {
|
| 97 |
+
opacity: 0;
|
| 98 |
+
pointer-events: none;
|
| 99 |
+
}
|
| 100 |
+
</style>
|
| 101 |
+
</head>
|
| 102 |
+
<body>
|
| 103 |
+
<div id="root">
|
| 104 |
+
<!-- Loading Screen -->
|
| 105 |
+
<div class="loading-screen" id="loading">
|
| 106 |
+
<div class="loading-text">
|
| 107 |
+
⟐ awakening the nexus ⟐<br/>
|
| 108 |
+
<span style="font-size: 0.7em; opacity: 0.8; font-family: 'Uncial Antiqua', serif;">
|
| 109 |
+
◦ growing organic pathways ◦
|
| 110 |
+
</span>
|
| 111 |
+
</div>
|
| 112 |
+
</div>
|
| 113 |
+
</div>
|
| 114 |
+
|
| 115 |
+
<script>
|
| 116 |
+
// Hide loading screen once React app is ready
|
| 117 |
+
window.addEventListener('load', () => {
|
| 118 |
+
setTimeout(() => {
|
| 119 |
+
const loading = document.getElementById('loading');
|
| 120 |
+
if (loading) {
|
| 121 |
+
loading.classList.add('hidden');
|
| 122 |
+
setTimeout(() => loading.remove(), 600);
|
| 123 |
+
}
|
| 124 |
+
}, 1200);
|
| 125 |
+
});
|
| 126 |
+
</script>
|
| 127 |
+
|
| 128 |
+
<script type="module" src="/src/main.tsx"></script>
|
| 129 |
+
</body>
|
| 130 |
+
</html>
|
main.js
ADDED
|
@@ -0,0 +1,1531 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as THREE from 'https://unpkg.com/three@0.136.0/build/three.module.js';
|
| 2 |
+
import { AudioLoader, AudioListener, Audio, AudioAnalyser } from 'https://unpkg.com/three@0.136.0/build/three.module.js';
|
| 3 |
+
import * as dat from 'https://cdn.jsdelivr.net/npm/dat.gui/build/dat.gui.module.js';
|
| 4 |
+
|
| 5 |
+
console.log('Script started');
|
| 6 |
+
|
| 7 |
+
// Cleanup function
|
| 8 |
+
function cleanupPreviousElements() {
|
| 9 |
+
const elementsToRemove = [
|
| 10 |
+
'#songSelect', '#playPause', '#volume',
|
| 11 |
+
'button', 'select', '.dg.main', 'canvas', '.controls-container',
|
| 12 |
+
'#volumeControl', '#audioControls', '.audio-control'
|
| 13 |
+
];
|
| 14 |
+
|
| 15 |
+
elementsToRemove.forEach(selector => {
|
| 16 |
+
document.querySelectorAll(selector).forEach(element => {
|
| 17 |
+
if (selector !== '#presetContainer') {
|
| 18 |
+
element.remove();
|
| 19 |
+
}
|
| 20 |
+
});
|
| 21 |
+
});
|
| 22 |
+
|
| 23 |
+
document.querySelectorAll('div').forEach(div => {
|
| 24 |
+
if ((div.id?.includes('control') ||
|
| 25 |
+
div.className?.includes('control') ||
|
| 26 |
+
div.id?.includes('audio') ||
|
| 27 |
+
div.className?.includes('audio')) &&
|
| 28 |
+
div.id !== 'presetContainer') {
|
| 29 |
+
div.remove();
|
| 30 |
+
}
|
| 31 |
+
});
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
cleanupPreviousElements();
|
| 35 |
+
console.log('Cleanup completed');
|
| 36 |
+
|
| 37 |
+
// Fixed position GUI and presets
|
| 38 |
+
const guiAndPresetsStyleFix = document.createElement('style');
|
| 39 |
+
guiAndPresetsStyleFix.textContent = `
|
| 40 |
+
/* Fixed position GUI spheres */
|
| 41 |
+
.dg.main {
|
| 42 |
+
position: absolute !important;
|
| 43 |
+
top: 10px !important;
|
| 44 |
+
right: 10px !important;
|
| 45 |
+
z-index: 1000 !important;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
#presetContainer {
|
| 49 |
+
position: fixed !important;
|
| 50 |
+
top: 10px !important;
|
| 51 |
+
left: 10px !important;
|
| 52 |
+
z-index: 1000 !important;
|
| 53 |
+
display: flex !important;
|
| 54 |
+
gap: 10px !important;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
#presetContainer input[type="text"] {
|
| 58 |
+
flex: 1 1 auto;
|
| 59 |
+
min-width: 150px;
|
| 60 |
+
padding: 5px;
|
| 61 |
+
border-radius: 3px;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
#presetContainer select,
|
| 65 |
+
#presetContainer button {
|
| 66 |
+
flex: 0 0 auto;
|
| 67 |
+
padding: 5px;
|
| 68 |
+
border-radius: 3px;
|
| 69 |
+
}
|
| 70 |
+
`;
|
| 71 |
+
document.head.appendChild(guiAndPresetsStyleFix);
|
| 72 |
+
|
| 73 |
+
// Scene setup
|
| 74 |
+
const scene = new THREE.Scene();
|
| 75 |
+
scene.background = new THREE.Color(0x000000);
|
| 76 |
+
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
| 77 |
+
camera.position.z = 2.5;
|
| 78 |
+
|
| 79 |
+
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
| 80 |
+
renderer.setSize(window.innerWidth, window.innerHeight);
|
| 81 |
+
document.body.appendChild(renderer.domElement);
|
| 82 |
+
|
| 83 |
+
console.log('Scene and renderer initialized');
|
| 84 |
+
|
| 85 |
+
// Audio setup
|
| 86 |
+
let audioContext;
|
| 87 |
+
let analyser;
|
| 88 |
+
let audioElement;
|
| 89 |
+
let sourceNode = null;
|
| 90 |
+
let micStream;
|
| 91 |
+
let micSource = null;
|
| 92 |
+
|
| 93 |
+
// Audio Controls Container
|
| 94 |
+
const controls = document.createElement('div');
|
| 95 |
+
controls.style.cssText = 'position: absolute; top: 10px; right: 310px; z-index: 1000; background: rgba(0,0,0,0.7); padding: 10px; border-radius: 5px;';
|
| 96 |
+
document.body.appendChild(controls);
|
| 97 |
+
|
| 98 |
+
const audioControls = document.createElement('div');
|
| 99 |
+
audioControls.style.cssText = 'display: flex; align-items: center; gap: 10px;';
|
| 100 |
+
controls.appendChild(audioControls);
|
| 101 |
+
|
| 102 |
+
const songSelect = document.createElement('select');
|
| 103 |
+
songSelect.style.cssText = 'padding: 5px; border-radius: 3px; background: #333; color: white; border: 1px solid #666;';
|
| 104 |
+
audioControls.appendChild(songSelect);
|
| 105 |
+
|
| 106 |
+
const playPause = document.createElement('button');
|
| 107 |
+
playPause.textContent = 'Play';
|
| 108 |
+
playPause.style.cssText = 'padding: 5px 15px; border-radius: 3px; background: #444; color: white; border: 1px solid #666;';
|
| 109 |
+
audioControls.appendChild(playPause);
|
| 110 |
+
|
| 111 |
+
const volumeControl = document.createElement('input');
|
| 112 |
+
volumeControl.type = 'range';
|
| 113 |
+
volumeControl.min = 0;
|
| 114 |
+
volumeControl.max = 1;
|
| 115 |
+
volumeControl.step = 0.1;
|
| 116 |
+
volumeControl.value = 0.5;
|
| 117 |
+
volumeControl.style.width = '100px';
|
| 118 |
+
audioControls.appendChild(volumeControl);
|
| 119 |
+
|
| 120 |
+
const timelineControl = document.createElement('input');
|
| 121 |
+
timelineControl.type = 'range';
|
| 122 |
+
timelineControl.min = 0;
|
| 123 |
+
timelineControl.max = 100;
|
| 124 |
+
timelineControl.step = 1;
|
| 125 |
+
timelineControl.value = 0;
|
| 126 |
+
timelineControl.style.width = '200px';
|
| 127 |
+
timelineControl.style.marginLeft = '10px';
|
| 128 |
+
audioControls.appendChild(timelineControl);
|
| 129 |
+
|
| 130 |
+
const inputToggle = document.createElement('button');
|
| 131 |
+
inputToggle.textContent = 'Use Mic';
|
| 132 |
+
inputToggle.style.cssText = 'padding: 5px 15px; border-radius: 3px; background: #444; color: white; border: 1px solid #666; margin-left: 10px;';
|
| 133 |
+
audioControls.appendChild(inputToggle);
|
| 134 |
+
|
| 135 |
+
let usingMic = false;
|
| 136 |
+
inputToggle.onclick = toggleInput;
|
| 137 |
+
|
| 138 |
+
playPause.onclick = togglePlay;
|
| 139 |
+
songSelect.onchange = changeSong;
|
| 140 |
+
|
| 141 |
+
console.log('Controls created');
|
| 142 |
+
|
| 143 |
+
// Noise generator
|
| 144 |
+
const noise = {
|
| 145 |
+
p: new Array(256).fill(0).map((_, i) => i),
|
| 146 |
+
perm: new Array(512),
|
| 147 |
+
|
| 148 |
+
init() {
|
| 149 |
+
for (let i = this.p.length - 1; i > 0; i--) {
|
| 150 |
+
const j = Math.floor(Math.random() * (i + 1));
|
| 151 |
+
[this.p[i], this.p[j]] = [this.p[j], this.p[i]];
|
| 152 |
+
}
|
| 153 |
+
for (let i = 0; i < 512; i++) {
|
| 154 |
+
this.perm[i] = this.p[i & 255];
|
| 155 |
+
}
|
| 156 |
+
},
|
| 157 |
+
|
| 158 |
+
fade(t) { return t * t * t * (t * (t * 6 - 15) + 10); },
|
| 159 |
+
lerp(t, a, b) { return a + t * (b - a); },
|
| 160 |
+
|
| 161 |
+
grad(hash, x, y, z) {
|
| 162 |
+
const h = hash & 15;
|
| 163 |
+
const u = h < 8 ? x : y;
|
| 164 |
+
const v = h < 4 ? y : h == 12 || h == 14 ? x : z;
|
| 165 |
+
return ((h & 1) == 0 ? u : -u) + ((h & 2) == 0 ? v : -v);
|
| 166 |
+
},
|
| 167 |
+
|
| 168 |
+
noise3D(x, y, z) {
|
| 169 |
+
const X = Math.floor(x) & 255;
|
| 170 |
+
const Y = Math.floor(y) & 255;
|
| 171 |
+
const Z = Math.floor(z) & 255;
|
| 172 |
+
|
| 173 |
+
x -= Math.floor(x);
|
| 174 |
+
y -= Math.floor(y);
|
| 175 |
+
z -= Math.floor(z);
|
| 176 |
+
|
| 177 |
+
const u = this.fade(x);
|
| 178 |
+
const v = this.fade(y);
|
| 179 |
+
const w = this.fade(z);
|
| 180 |
+
|
| 181 |
+
const A = this.perm[X] + Y;
|
| 182 |
+
const AA = this.perm[A] + Z;
|
| 183 |
+
const AB = this.perm[A + 1] + Z;
|
| 184 |
+
const B = this.perm[X + 1] + Y;
|
| 185 |
+
const BA = this.perm[B] + Z;
|
| 186 |
+
const BB = this.perm[B + 1] + Z;
|
| 187 |
+
|
| 188 |
+
return this.lerp(w, this.lerp(v, this.lerp(u, this.grad(this.perm[AA], x, y, z),
|
| 189 |
+
this.grad(this.perm[BA], x-1, y, z)),
|
| 190 |
+
this.lerp(u, this.grad(this.perm[AB], x, y-1, z),
|
| 191 |
+
this.grad(this.perm[BB], x-1, y-1, z))),
|
| 192 |
+
this.lerp(v, this.lerp(u, this.grad(this.perm[AA+1], x, y, z-1),
|
| 193 |
+
this.grad(this.perm[BA+1], x-1, y, z-1)),
|
| 194 |
+
this.lerp(u, this.grad(this.perm[AB+1], x, y-1, z-1),
|
| 195 |
+
this.grad(this.perm[BB+1], x-1, y-1, z-1))));
|
| 196 |
+
}
|
| 197 |
+
};
|
| 198 |
+
noise.init();
|
| 199 |
+
console.log('Noise initialized');
|
| 200 |
+
|
| 201 |
+
// Beat manager
|
| 202 |
+
const beatManager = {
|
| 203 |
+
currentWaveRadius: 0,
|
| 204 |
+
waveStrength: 0,
|
| 205 |
+
isWaveActive: false,
|
| 206 |
+
|
| 207 |
+
triggerWave(rangeEnergy) {
|
| 208 |
+
const maxEnergy = 255;
|
| 209 |
+
const energyExcess = rangeEnergy - 200;
|
| 210 |
+
this.waveStrength = (energyExcess / (maxEnergy - 200)) * 20.0;
|
| 211 |
+
this.currentWaveRadius = 0;
|
| 212 |
+
this.isWaveActive = true;
|
| 213 |
+
},
|
| 214 |
+
|
| 215 |
+
update(deltaTime) {
|
| 216 |
+
if (this.isWaveActive) {
|
| 217 |
+
this.currentWaveRadius += deltaTime * 1.0;
|
| 218 |
+
this.waveStrength *= 0.98;
|
| 219 |
+
|
| 220 |
+
if (this.currentWaveRadius > 1.0 || this.waveStrength < 0.1) {
|
| 221 |
+
this.isWaveActive = false;
|
| 222 |
+
}
|
| 223 |
+
}
|
| 224 |
+
},
|
| 225 |
+
|
| 226 |
+
getWaveForce(position) {
|
| 227 |
+
if (!this.isWaveActive) return 0;
|
| 228 |
+
const distanceFromCenter = position.length();
|
| 229 |
+
const distanceFromWave = Math.abs(distanceFromCenter - this.currentWaveRadius);
|
| 230 |
+
if (distanceFromWave < 0.1) {
|
| 231 |
+
return this.waveStrength * Math.exp(-distanceFromWave * 10);
|
| 232 |
+
}
|
| 233 |
+
return 0;
|
| 234 |
+
}
|
| 235 |
+
};
|
| 236 |
+
|
| 237 |
+
async function toggleInput() {
|
| 238 |
+
|
| 239 |
+
if (!audioContext) {
|
| 240 |
+
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
| 241 |
+
analyser = audioContext.createAnalyser();
|
| 242 |
+
analyser.fftSize = 2048;
|
| 243 |
+
}
|
| 244 |
+
await audioContext.resume();
|
| 245 |
+
|
| 246 |
+
if (usingMic) {
|
| 247 |
+
usingMic = false;
|
| 248 |
+
inputToggle.textContent = 'Use Mic';
|
| 249 |
+
songSelect.disabled = false;
|
| 250 |
+
playPause.disabled = false;
|
| 251 |
+
volumeControl.disabled = false;
|
| 252 |
+
timelineControl.disabled = false;
|
| 253 |
+
|
| 254 |
+
if (micSource) {
|
| 255 |
+
micSource.disconnect();
|
| 256 |
+
micSource = null;
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
if (sourceNode) {
|
| 260 |
+
sourceNode.connect(analyser);
|
| 261 |
+
}
|
| 262 |
+
analyser.connect(audioContext.destination);
|
| 263 |
+
|
| 264 |
+
} else {
|
| 265 |
+
usingMic = true;
|
| 266 |
+
inputToggle.textContent = 'Use Player';
|
| 267 |
+
songSelect.disabled = true;
|
| 268 |
+
playPause.disabled = true;
|
| 269 |
+
volumeControl.disabled = true;
|
| 270 |
+
timelineControl.disabled = true;
|
| 271 |
+
|
| 272 |
+
try {
|
| 273 |
+
micStream = await navigator.mediaDevices.getUserMedia({
|
| 274 |
+
audio: {
|
| 275 |
+
echoCancellation: true,
|
| 276 |
+
noiseSuppression: true,
|
| 277 |
+
autoGainControl: false
|
| 278 |
+
}
|
| 279 |
+
});
|
| 280 |
+
|
| 281 |
+
if (sourceNode) {
|
| 282 |
+
sourceNode.disconnect();
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
micSource = audioContext.createMediaStreamSource(micStream);
|
| 286 |
+
micSource.connect(analyser);
|
| 287 |
+
analyser.disconnect(audioContext.destination);
|
| 288 |
+
|
| 289 |
+
console.log('Microphone is active');
|
| 290 |
+
|
| 291 |
+
} catch (error) {
|
| 292 |
+
console.error("Microphone access failed:", error.name, error.message);
|
| 293 |
+
usingMic = false;
|
| 294 |
+
inputToggle.textContent = 'Use Mic';
|
| 295 |
+
}
|
| 296 |
+
}
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
async function initAudio() {
|
| 300 |
+
try {
|
| 301 |
+
if (!audioContext) {
|
| 302 |
+
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
| 303 |
+
analyser = audioContext.createAnalyser();
|
| 304 |
+
analyser.fftSize = 2048;
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
if (!usingMic) {
|
| 308 |
+
if (!audioElement) {
|
| 309 |
+
audioElement = document.createElement('audio');
|
| 310 |
+
audioElement.crossOrigin = "anonymous";
|
| 311 |
+
audioElement.volume = volumeControl.value;
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
try {
|
| 315 |
+
if (!sourceNode) {
|
| 316 |
+
sourceNode = audioContext.createMediaElementSource(audioElement);
|
| 317 |
+
sourceNode.connect(analyser);
|
| 318 |
+
analyser.connect(audioContext.destination);
|
| 319 |
+
} else {
|
| 320 |
+
sourceNode.disconnect();
|
| 321 |
+
sourceNode.connect(analyser);
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
} catch (error) {
|
| 325 |
+
console.error("Failed to connect audio element to analyser:", error.name, error.message);
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
try {
|
| 329 |
+
const response = await fetch('Songs/');
|
| 330 |
+
const text = await response.text();
|
| 331 |
+
|
| 332 |
+
const parser = new DOMParser();
|
| 333 |
+
const doc = parser.parseFromString(text, 'text/html');
|
| 334 |
+
|
| 335 |
+
const files = Array.from(doc.querySelectorAll('a'))
|
| 336 |
+
.map(a => decodeURIComponent(a.href))
|
| 337 |
+
.filter(href => href.match(/\.(mp3|wav|ogg)$/i))
|
| 338 |
+
.map(href => {
|
| 339 |
+
const fileName = href.split('/').pop();
|
| 340 |
+
return {
|
| 341 |
+
path: `Songs/${fileName}`,
|
| 342 |
+
name: fileName.replace(/\.(mp3|wav|ogg)$/i, '')
|
| 343 |
+
};
|
| 344 |
+
});
|
| 345 |
+
|
| 346 |
+
songSelect.innerHTML = '';
|
| 347 |
+
|
| 348 |
+
files.forEach(file => {
|
| 349 |
+
const option = document.createElement('option');
|
| 350 |
+
option.value = file.path;
|
| 351 |
+
option.textContent = file.name;
|
| 352 |
+
songSelect.appendChild(option);
|
| 353 |
+
});
|
| 354 |
+
|
| 355 |
+
if (files.length > 0 && !audioElement.src) {
|
| 356 |
+
audioElement.src = files[0].path;
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
} catch (error) {
|
| 360 |
+
console.error("Failed to load song list:", error);
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
volumeControl.oninput = e => {
|
| 364 |
+
if (audioElement) {
|
| 365 |
+
audioElement.volume = e.target.value;
|
| 366 |
+
}
|
| 367 |
+
};
|
| 368 |
+
|
| 369 |
+
setupTimelineControl();
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
console.log('Audio initialized');
|
| 373 |
+
|
| 374 |
+
} catch (error) {
|
| 375 |
+
console.error("Audio initialization failed:", error);
|
| 376 |
+
}
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
function getAudioData(sphere) {
|
| 380 |
+
if (!analyser || (!audioContext && !usingMic)) return {
|
| 381 |
+
average: 0,
|
| 382 |
+
frequencies: new Float32Array(),
|
| 383 |
+
peakDetected: false,
|
| 384 |
+
rangeEnergy: 0
|
| 385 |
+
};
|
| 386 |
+
|
| 387 |
+
try {
|
| 388 |
+
const frequencies = new Uint8Array(analyser.frequencyBinCount);
|
| 389 |
+
analyser.getByteFrequencyData(frequencies);
|
| 390 |
+
|
| 391 |
+
const gainMultiplier = sphere.params.gainMultiplier;
|
| 392 |
+
frequencies.forEach((value, index) => {
|
| 393 |
+
frequencies[index] = Math.min(value * gainMultiplier, 255);
|
| 394 |
+
});
|
| 395 |
+
|
| 396 |
+
const frequencyToIndex = (frequency) => Math.round(frequency / (audioContext.sampleRate / 2) * analyser.frequencyBinCount);
|
| 397 |
+
|
| 398 |
+
const minFreqIndex = frequencyToIndex(sphere.params.minFrequency);
|
| 399 |
+
const maxFreqIndex = frequencyToIndex(sphere.params.maxFrequency);
|
| 400 |
+
const frequencyRange = frequencies.slice(minFreqIndex, maxFreqIndex + 1);
|
| 401 |
+
const rangeEnergy = frequencyRange.reduce((a, b) => a + b, 0) / frequencyRange.length;
|
| 402 |
+
|
| 403 |
+
const minFreqBeatIndex = frequencyToIndex(sphere.params.minFrequencyBeat); // Nové pásmo pro beaty
|
| 404 |
+
const maxFreqBeatIndex = frequencyToIndex(sphere.params.maxFrequencyBeat);
|
| 405 |
+
const frequencyRangeBeat = frequencies.slice(minFreqBeatIndex, maxFreqBeatIndex + 1);
|
| 406 |
+
const rangeEnergyBeat = frequencyRangeBeat.reduce((a, b) => a + b, 0) / frequencyRangeBeat.length;
|
| 407 |
+
|
| 408 |
+
sphere.peakDetection.energyHistory.push(rangeEnergy);
|
| 409 |
+
if (sphere.peakDetection.energyHistory.length > sphere.peakDetection.historyLength) {
|
| 410 |
+
sphere.peakDetection.energyHistory.shift();
|
| 411 |
+
}
|
| 412 |
+
|
| 413 |
+
const averageEnergy = sphere.peakDetection.energyHistory.reduce((a, b) => a + b, 0) /
|
| 414 |
+
sphere.peakDetection.energyHistory.length;
|
| 415 |
+
|
| 416 |
+
const now = performance.now();
|
| 417 |
+
const peakDetected = rangeEnergy > averageEnergy * sphere.params.peakSensitivity &&
|
| 418 |
+
now - sphere.peakDetection.lastPeakTime > sphere.peakDetection.minTimeBetweenPeaks;
|
| 419 |
+
|
| 420 |
+
if (peakDetected) {
|
| 421 |
+
sphere.peakDetection.lastPeakTime = now;
|
| 422 |
+
console.log(`Sphere ${sphere.index + 1} PEAK DETECTED! Energy: ${rangeEnergy}, Average: ${averageEnergy}`);
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
|
| 426 |
+
return {
|
| 427 |
+
average: rangeEnergy / 255,
|
| 428 |
+
frequencies,
|
| 429 |
+
peakDetected,
|
| 430 |
+
rangeEnergy: rangeEnergy,
|
| 431 |
+
rangeEnergyBeat: rangeEnergyBeat
|
| 432 |
+
};
|
| 433 |
+
|
| 434 |
+
} catch (error) {
|
| 435 |
+
console.error("Audio analysis failed:", error);
|
| 436 |
+
return {
|
| 437 |
+
average: 0,
|
| 438 |
+
frequencies: new Float32Array(),
|
| 439 |
+
peakDetected: false,
|
| 440 |
+
rangeEnergy: 0
|
| 441 |
+
};
|
| 442 |
+
}
|
| 443 |
+
}
|
| 444 |
+
|
| 445 |
+
|
| 446 |
+
function updateTimeline() {
|
| 447 |
+
if (audioElement && !audioElement.paused && audioElement.duration) {
|
| 448 |
+
const percent = (audioElement.currentTime / audioElement.duration) * 100;
|
| 449 |
+
timelineControl.value = percent;
|
| 450 |
+
}
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
function setupTimelineControl() {
|
| 454 |
+
audioElement.addEventListener('loadedmetadata', () => {
|
| 455 |
+
timelineControl.value = 0;
|
| 456 |
+
console.log('Song duration:', audioElement.duration);
|
| 457 |
+
});
|
| 458 |
+
|
| 459 |
+
audioElement.addEventListener('timeupdate', updateTimeline);
|
| 460 |
+
|
| 461 |
+
timelineControl.addEventListener('input', (e) => {
|
| 462 |
+
if (audioElement.duration) {
|
| 463 |
+
const time = (e.target.value / 100) * audioElement.duration;
|
| 464 |
+
audioElement.currentTime = time;
|
| 465 |
+
}
|
| 466 |
+
});
|
| 467 |
+
}
|
| 468 |
+
|
| 469 |
+
function togglePlay() {
|
| 470 |
+
if (usingMic) return;
|
| 471 |
+
|
| 472 |
+
if (audioContext.state === 'suspended') {
|
| 473 |
+
audioContext.resume();
|
| 474 |
+
}
|
| 475 |
+
|
| 476 |
+
if (!sourceNode && audioElement) {
|
| 477 |
+
try {
|
| 478 |
+
sourceNode = audioContext.createMediaElementSource(audioElement);
|
| 479 |
+
sourceNode.connect(analyser);
|
| 480 |
+
analyser.connect(audioContext.destination);
|
| 481 |
+
} catch (error) {
|
| 482 |
+
console.error("Audio connection failed:", error);
|
| 483 |
+
return;
|
| 484 |
+
}
|
| 485 |
+
}
|
| 486 |
+
|
| 487 |
+
if (audioElement.paused) {
|
| 488 |
+
audioElement.play()
|
| 489 |
+
.then(() => playPause.textContent = 'Pause')
|
| 490 |
+
.catch(error => console.error("Playback failed:", error));
|
| 491 |
+
} else {
|
| 492 |
+
audioElement.pause();
|
| 493 |
+
playPause.textContent = 'Play';
|
| 494 |
+
}
|
| 495 |
+
}
|
| 496 |
+
|
| 497 |
+
function changeSong() {
|
| 498 |
+
if (audioContext.state === 'suspended') {
|
| 499 |
+
audioContext.resume();
|
| 500 |
+
}
|
| 501 |
+
|
| 502 |
+
audioElement.src = songSelect.value;
|
| 503 |
+
audioElement.load();
|
| 504 |
+
|
| 505 |
+
if (!sourceNode) {
|
| 506 |
+
try {
|
| 507 |
+
sourceNode = audioContext.createMediaElementSource(audioElement);
|
| 508 |
+
sourceNode.connect(analyser);
|
| 509 |
+
analyser.connect(audioContext.destination);
|
| 510 |
+
} catch (error) {
|
| 511 |
+
console.error("Audio connection failed:", error);
|
| 512 |
+
return;
|
| 513 |
+
}
|
| 514 |
+
}
|
| 515 |
+
|
| 516 |
+
audioElement.play()
|
| 517 |
+
.then(() => {
|
| 518 |
+
playPause.textContent = 'Pause';
|
| 519 |
+
console.log("Playback started");
|
| 520 |
+
})
|
| 521 |
+
.catch(error => console.error("Playback failed:", error));
|
| 522 |
+
|
| 523 |
+
timelineControl.value = 0;
|
| 524 |
+
}
|
| 525 |
+
|
| 526 |
+
function generateNewNoiseScale(params, lastNoiseScale) {
|
| 527 |
+
if (!params.dynamicNoiseScale) {
|
| 528 |
+
return params.noiseScale;
|
| 529 |
+
}
|
| 530 |
+
|
| 531 |
+
let { minNoiseScale, maxNoiseScale, noiseStep } = params;
|
| 532 |
+
|
| 533 |
+
// --- PŘIDANÁ POJISTKA A DROBNÉ LOGY ---
|
| 534 |
+
if (minNoiseScale >= maxNoiseScale) {
|
| 535 |
+
console.warn(`Fixing minNoiseScale (${minNoiseScale}) >= maxNoiseScale (${maxNoiseScale}).`);
|
| 536 |
+
maxNoiseScale = minNoiseScale + 0.1; // Natvrdo posunout, aby byl rozdíl aspoň 0.1
|
| 537 |
+
}
|
| 538 |
+
|
| 539 |
+
let range = maxNoiseScale - minNoiseScale;
|
| 540 |
+
if (range < 0.1) {
|
| 541 |
+
console.warn(`Range < 0.1 => Forcing minimal range = 0.1`);
|
| 542 |
+
range = 0.1;
|
| 543 |
+
maxNoiseScale = minNoiseScale + range;
|
| 544 |
+
}
|
| 545 |
+
|
| 546 |
+
if (noiseStep > range) {
|
| 547 |
+
console.warn(`noiseStep (${noiseStep}) > range (${range}) => Forcing noiseStep = range / 2`);
|
| 548 |
+
noiseStep = range / 2;
|
| 549 |
+
}
|
| 550 |
+
|
| 551 |
+
// LastNoiseScale valid range
|
| 552 |
+
lastNoiseScale = Math.max(minNoiseScale, Math.min(lastNoiseScale, maxNoiseScale));
|
| 553 |
+
|
| 554 |
+
const stepsUp = Math.floor((maxNoiseScale - lastNoiseScale) / noiseStep);
|
| 555 |
+
const stepsDown = Math.floor((lastNoiseScale - minNoiseScale) / noiseStep);
|
| 556 |
+
|
| 557 |
+
if (stepsUp === 0 && stepsDown === 0) {
|
| 558 |
+
return lastNoiseScale;
|
| 559 |
+
}
|
| 560 |
+
|
| 561 |
+
const direction = Math.random() < 0.5 && stepsDown > 0 ? -1 : 1;
|
| 562 |
+
const steps = direction === 1
|
| 563 |
+
? Math.floor(Math.random() * (stepsUp + 1))
|
| 564 |
+
: Math.floor(Math.random() * (stepsDown + 1));
|
| 565 |
+
|
| 566 |
+
let newValue = lastNoiseScale + direction * steps * noiseStep;
|
| 567 |
+
|
| 568 |
+
newValue = Math.max(minNoiseScale, Math.min(newValue, maxNoiseScale));
|
| 569 |
+
|
| 570 |
+
return newValue;
|
| 571 |
+
}
|
| 572 |
+
|
| 573 |
+
// Reinit particles
|
| 574 |
+
function reinitializeParticlesForSphere(sphere, sphereParams, sphereGeometry) {
|
| 575 |
+
console.log(`Reinitializing sphere ${sphere.index + 1} with ${sphereParams.particleCount} particles`);
|
| 576 |
+
|
| 577 |
+
const newPositions = new Float32Array(sphereParams.particleCount * 3);
|
| 578 |
+
const newColors = new Float32Array(sphereParams.particleCount * 3);
|
| 579 |
+
const newVelocities = new Float32Array(sphereParams.particleCount * 3);
|
| 580 |
+
const newBasePositions = new Float32Array(sphereParams.particleCount * 3);
|
| 581 |
+
const newLifetimes = new Float32Array(sphereParams.particleCount);
|
| 582 |
+
const newMaxLifetimes = new Float32Array(sphereParams.particleCount);
|
| 583 |
+
const newBeatEffects = new Float32Array(sphereParams.particleCount);
|
| 584 |
+
|
| 585 |
+
for (let i = 0; i < sphereParams.particleCount; i++) {
|
| 586 |
+
const i3 = i * 3;
|
| 587 |
+
const radius = THREE.MathUtils.lerp(0, sphereParams.sphereRadius, sphereParams.innerSphereRadius);
|
| 588 |
+
const theta = Math.random() * Math.PI * 2;
|
| 589 |
+
const phi = Math.acos(2 * Math.random() - 1);
|
| 590 |
+
const r = Math.cbrt(Math.random()) * radius;
|
| 591 |
+
|
| 592 |
+
const x = r * Math.sin(phi) * Math.cos(theta);
|
| 593 |
+
const y = r * Math.sin(phi) * Math.sin(theta);
|
| 594 |
+
const z = r * Math.cos(phi);
|
| 595 |
+
|
| 596 |
+
newPositions[i3] = x;
|
| 597 |
+
newPositions[i3 + 1] = y;
|
| 598 |
+
newPositions[i3 + 2] = z;
|
| 599 |
+
|
| 600 |
+
newBasePositions[i3] = x;
|
| 601 |
+
newBasePositions[i3 + 1] = y;
|
| 602 |
+
newBasePositions[i3 + 2] = z;
|
| 603 |
+
|
| 604 |
+
newVelocities[i3] = 0;
|
| 605 |
+
newVelocities[i3 + 1] = 0;
|
| 606 |
+
newVelocities[i3 + 2] = 0;
|
| 607 |
+
|
| 608 |
+
const lt = Math.random() * sphereParams.particleLifetime;
|
| 609 |
+
newLifetimes[i] = lt;
|
| 610 |
+
newMaxLifetimes[i] = lt;
|
| 611 |
+
|
| 612 |
+
newBeatEffects[i] = 0;
|
| 613 |
+
}
|
| 614 |
+
|
| 615 |
+
sphereGeometry.setAttribute('position', new THREE.BufferAttribute(newPositions, 3));
|
| 616 |
+
sphereGeometry.setAttribute('color', new THREE.BufferAttribute(newColors, 3));
|
| 617 |
+
|
| 618 |
+
updateColorsForSphere(sphereParams, sphereGeometry, newColors);
|
| 619 |
+
|
| 620 |
+
return {
|
| 621 |
+
newPositions,
|
| 622 |
+
newColors,
|
| 623 |
+
newVelocities,
|
| 624 |
+
newBasePositions,
|
| 625 |
+
newLifetimes,
|
| 626 |
+
newMaxLifetimes,
|
| 627 |
+
newBeatEffects
|
| 628 |
+
};
|
| 629 |
+
}
|
| 630 |
+
|
| 631 |
+
function updateColorsForSphere(sphereParams, sphereGeometry, sphereColors) {
|
| 632 |
+
const color1 = new THREE.Color(sphereParams.colorStart);
|
| 633 |
+
const color2 = new THREE.Color(sphereParams.colorEnd);
|
| 634 |
+
|
| 635 |
+
for (let i = 0; i < sphereParams.particleCount; i++) {
|
| 636 |
+
const t = i / sphereParams.particleCount;
|
| 637 |
+
sphereColors[i * 3] = color1.r * (1 - t) + color2.r * t;
|
| 638 |
+
sphereColors[i * 3 + 1] = color1.g * (1 - t) + color2.g * t;
|
| 639 |
+
sphereColors[i * 3 + 2] = color1.b * (1 - t) + color2.b * t;
|
| 640 |
+
}
|
| 641 |
+
sphereGeometry.attributes.color.needsUpdate = true;
|
| 642 |
+
}
|
| 643 |
+
|
| 644 |
+
// Preset management
|
| 645 |
+
const presets = JSON.parse(localStorage.getItem('presets')) || {}; // Uložené presety
|
| 646 |
+
const defaultParams = []; // Pro ukládání výchozích hodnot každé sféry
|
| 647 |
+
|
| 648 |
+
// HTML elements presets
|
| 649 |
+
|
| 650 |
+
let presetContainer = document.querySelector('#presetContainer');
|
| 651 |
+
let presetInput, saveButton, resetButton, presetSelect, deleteButton, exportButton, importButton;
|
| 652 |
+
|
| 653 |
+
if (!presetContainer) {
|
| 654 |
+
presetContainer = document.createElement('div');
|
| 655 |
+
presetContainer.id = 'presetContainer';
|
| 656 |
+
presetContainer.style.cssText = `
|
| 657 |
+
position: fixed !important;
|
| 658 |
+
top: 10px !important;
|
| 659 |
+
left: 10px !important;
|
| 660 |
+
z-index: 1000 !important;
|
| 661 |
+
display: flex !important;
|
| 662 |
+
gap: 10px !important;
|
| 663 |
+
`;
|
| 664 |
+
|
| 665 |
+
presetInput = document.createElement('input');
|
| 666 |
+
presetInput.type = 'text';
|
| 667 |
+
presetInput.placeholder = 'Preset name';
|
| 668 |
+
presetInput.style.cssText = 'padding: 5px; border-radius: 3px;';
|
| 669 |
+
|
| 670 |
+
saveButton = document.createElement('button');
|
| 671 |
+
saveButton.textContent = 'Save';
|
| 672 |
+
saveButton.style.cssText = 'padding: 5px 10px; border-radius: 3px; background: #444; color: white; border: 1px solid #666;';
|
| 673 |
+
|
| 674 |
+
resetButton = document.createElement('button');
|
| 675 |
+
resetButton.textContent = 'Reset';
|
| 676 |
+
resetButton.style.cssText = 'padding: 5px 10px; border-radius: 3px; background: #444; color: white; border: 1px solid #666;';
|
| 677 |
+
|
| 678 |
+
presetSelect = document.createElement('select');
|
| 679 |
+
presetSelect.style.cssText = 'padding: 5px; border-radius: 3px; background: #333; color: white; border: 1px solid #666;';
|
| 680 |
+
|
| 681 |
+
deleteButton = document.createElement('button');
|
| 682 |
+
deleteButton.textContent = 'Delete';
|
| 683 |
+
deleteButton.style.cssText = 'padding: 5px 10px; border-radius: 3px; background: #444; color: white; border: 1px solid #666;';
|
| 684 |
+
|
| 685 |
+
exportButton = document.createElement('button');
|
| 686 |
+
exportButton.textContent = 'Export Presets';
|
| 687 |
+
exportButton.style.cssText = 'padding: 5px 10px; border-radius: 3px; background: #444; color: white; border: 1px solid #666;';
|
| 688 |
+
|
| 689 |
+
importButton = document.createElement('button');
|
| 690 |
+
importButton.textContent = 'Import Presets';
|
| 691 |
+
importButton.style.cssText = 'padding: 5px 10px; border-radius: 3px; background: #444; color: white; border: 1px solid #666;';
|
| 692 |
+
|
| 693 |
+
presetContainer.appendChild(presetInput);
|
| 694 |
+
presetContainer.appendChild(saveButton);
|
| 695 |
+
presetContainer.appendChild(resetButton);
|
| 696 |
+
presetContainer.appendChild(deleteButton);
|
| 697 |
+
presetContainer.appendChild(exportButton);
|
| 698 |
+
presetContainer.appendChild(importButton);
|
| 699 |
+
presetContainer.appendChild(presetSelect);
|
| 700 |
+
|
| 701 |
+
document.body.appendChild(presetContainer);
|
| 702 |
+
}
|
| 703 |
+
|
| 704 |
+
// Save presets logic
|
| 705 |
+
saveButton.onclick = () => {
|
| 706 |
+
const presetName = presetInput.value.trim();
|
| 707 |
+
if (!presetName) return;
|
| 708 |
+
presets[presetName] = spheres.map(sphere => JSON.parse(JSON.stringify(sphere.params)));
|
| 709 |
+
localStorage.setItem('presets', JSON.stringify(presets));
|
| 710 |
+
updatePresetOptions();
|
| 711 |
+
};
|
| 712 |
+
|
| 713 |
+
// Reset logic
|
| 714 |
+
resetButton.onclick = () => {
|
| 715 |
+
spheres.forEach((sphere, index) => {
|
| 716 |
+
const previousParticleCount = sphere.params.particleCount; // Uložíme původní počet částic
|
| 717 |
+
|
| 718 |
+
Object.assign(sphere.params, defaultParams[index]);
|
| 719 |
+
sphere.particleSystem.visible = sphere.params.enabled;
|
| 720 |
+
|
| 721 |
+
if (sphere.params.particleCount !== previousParticleCount) {
|
| 722 |
+
const {
|
| 723 |
+
newPositions,
|
| 724 |
+
newColors,
|
| 725 |
+
newVelocities,
|
| 726 |
+
newBasePositions,
|
| 727 |
+
newLifetimes,
|
| 728 |
+
newMaxLifetimes,
|
| 729 |
+
newBeatEffects
|
| 730 |
+
} = reinitializeParticlesForSphere(sphere, sphere.params, sphere.geometry);
|
| 731 |
+
|
| 732 |
+
sphere.positions = newPositions;
|
| 733 |
+
sphere.colors = newColors;
|
| 734 |
+
sphere.velocities = newVelocities;
|
| 735 |
+
sphere.basePositions = newBasePositions;
|
| 736 |
+
sphere.lifetimes = newLifetimes;
|
| 737 |
+
sphere.maxLifetimes = newMaxLifetimes;
|
| 738 |
+
sphere.beatEffects = newBeatEffects;
|
| 739 |
+
|
| 740 |
+
sphere.geometry.attributes.position.needsUpdate = true;
|
| 741 |
+
sphere.geometry.attributes.color.needsUpdate = true;
|
| 742 |
+
}
|
| 743 |
+
|
| 744 |
+
const sphereFolder = mainGui.__folders[`Sphere ${index + 1}`];
|
| 745 |
+
if (sphereFolder) {
|
| 746 |
+
sphereFolder.__controllers.forEach(controller => controller.updateDisplay());
|
| 747 |
+
}
|
| 748 |
+
});
|
| 749 |
+
|
| 750 |
+
console.log("Parameters reset to default values");
|
| 751 |
+
mainGui.updateDisplay();
|
| 752 |
+
};
|
| 753 |
+
|
| 754 |
+
// Delete preset logic
|
| 755 |
+
deleteButton.onclick = () => {
|
| 756 |
+
const presetName = presetSelect.value;
|
| 757 |
+
if (!presetName) {
|
| 758 |
+
console.warn('Žádný preset není vybraný.');
|
| 759 |
+
return;
|
| 760 |
+
}
|
| 761 |
+
|
| 762 |
+
const sure = confirm(`Skutečně smazat preset "${presetName}"?`);
|
| 763 |
+
if (!sure) return;
|
| 764 |
+
|
| 765 |
+
delete presets[presetName];
|
| 766 |
+
localStorage.setItem('presets', JSON.stringify(presets));
|
| 767 |
+
updatePresetOptions();
|
| 768 |
+
|
| 769 |
+
presetSelect.value = '';
|
| 770 |
+
presetInput.value = '';
|
| 771 |
+
|
| 772 |
+
console.log(`Preset "${presetName}" byl smazán.`);
|
| 773 |
+
};
|
| 774 |
+
|
| 775 |
+
// Export presets
|
| 776 |
+
exportButton.onclick = () => {
|
| 777 |
+
const presetData = JSON.stringify(presets, null, 2);
|
| 778 |
+
const blob = new Blob([presetData], { type: 'application/json' });
|
| 779 |
+
const url = URL.createObjectURL(blob);
|
| 780 |
+
const link = document.createElement('a');
|
| 781 |
+
link.href = url;
|
| 782 |
+
link.download = 'particula_presets.json';
|
| 783 |
+
link.click();
|
| 784 |
+
URL.revokeObjectURL(url);
|
| 785 |
+
};
|
| 786 |
+
|
| 787 |
+
// Import presets
|
| 788 |
+
importButton.onclick = () => {
|
| 789 |
+
const fileInput = document.createElement('input');
|
| 790 |
+
fileInput.type = 'file';
|
| 791 |
+
fileInput.accept = 'application/json';
|
| 792 |
+
fileInput.onchange = (event) => {
|
| 793 |
+
const file = event.target.files[0];
|
| 794 |
+
const reader = new FileReader();
|
| 795 |
+
reader.onload = (e) => {
|
| 796 |
+
const importedPresets = JSON.parse(e.target.result);
|
| 797 |
+
Object.assign(presets, importedPresets);
|
| 798 |
+
localStorage.setItem('presets', JSON.stringify(presets));
|
| 799 |
+
updatePresetOptions();
|
| 800 |
+
console.log('Presety byly úspěšně importovány.');
|
| 801 |
+
};
|
| 802 |
+
reader.readAsText(file);
|
| 803 |
+
};
|
| 804 |
+
fileInput.click();
|
| 805 |
+
};
|
| 806 |
+
|
| 807 |
+
// Upload presets logic
|
| 808 |
+
presetSelect.onchange = () => {
|
| 809 |
+
const presetName = presetSelect.value;
|
| 810 |
+
if (!presetName) return;
|
| 811 |
+
const preset = presets[presetName];
|
| 812 |
+
if (!preset) return;
|
| 813 |
+
|
| 814 |
+
spheres.forEach((sphere, index) => {
|
| 815 |
+
const previousParticleCount = sphere.params.particleCount; // Původní počet částic
|
| 816 |
+
|
| 817 |
+
Object.assign(sphere.params, preset[index]);
|
| 818 |
+
|
| 819 |
+
if (!('minFrequencyBeat' in sphere.params)) {
|
| 820 |
+
sphere.params.minFrequencyBeat = sphere.params.minFrequency;
|
| 821 |
+
}
|
| 822 |
+
if (!('maxFrequencyBeat' in sphere.params)) {
|
| 823 |
+
sphere.params.maxFrequencyBeat = sphere.params.maxFrequency;
|
| 824 |
+
}
|
| 825 |
+
|
| 826 |
+
if (sphere.params.minNoiseScale >= sphere.params.maxNoiseScale) {
|
| 827 |
+
console.warn(`Preset fix: minNoiseScale (${sphere.params.minNoiseScale}) >= maxNoiseScale (${sphere.params.maxNoiseScale}).`);
|
| 828 |
+
sphere.params.maxNoiseScale = sphere.params.minNoiseScale + 0.1;
|
| 829 |
+
}
|
| 830 |
+
|
| 831 |
+
if (sphere.params.particleCount !== previousParticleCount) {
|
| 832 |
+
const {
|
| 833 |
+
newPositions,
|
| 834 |
+
newColors,
|
| 835 |
+
newVelocities,
|
| 836 |
+
newBasePositions,
|
| 837 |
+
newLifetimes,
|
| 838 |
+
newMaxLifetimes,
|
| 839 |
+
newBeatEffects
|
| 840 |
+
} = reinitializeParticlesForSphere(sphere, sphere.params, sphere.geometry);
|
| 841 |
+
|
| 842 |
+
sphere.positions = newPositions;
|
| 843 |
+
sphere.colors = newColors;
|
| 844 |
+
sphere.velocities = newVelocities;
|
| 845 |
+
sphere.basePositions = newBasePositions;
|
| 846 |
+
sphere.lifetimes = newLifetimes;
|
| 847 |
+
sphere.maxLifetimes = newMaxLifetimes;
|
| 848 |
+
sphere.beatEffects = newBeatEffects;
|
| 849 |
+
|
| 850 |
+
sphere.geometry.attributes.position.needsUpdate = true;
|
| 851 |
+
sphere.geometry.attributes.color.needsUpdate = true;
|
| 852 |
+
}
|
| 853 |
+
|
| 854 |
+
sphere.particleSystem.visible = sphere.params.enabled;
|
| 855 |
+
});
|
| 856 |
+
|
| 857 |
+
mainGui.updateDisplay();
|
| 858 |
+
};
|
| 859 |
+
|
| 860 |
+
// Roll presets
|
| 861 |
+
function updatePresetOptions() {
|
| 862 |
+
while (presetSelect.firstChild) {
|
| 863 |
+
presetSelect.removeChild(presetSelect.firstChild);
|
| 864 |
+
}
|
| 865 |
+
|
| 866 |
+
const defaultOption = document.createElement('option');
|
| 867 |
+
defaultOption.textContent = 'Select preset';
|
| 868 |
+
defaultOption.value = '';
|
| 869 |
+
presetSelect.appendChild(defaultOption);
|
| 870 |
+
|
| 871 |
+
Object.keys(presets).forEach(name => {
|
| 872 |
+
const option = document.createElement('option');
|
| 873 |
+
option.textContent = name;
|
| 874 |
+
option.value = name;
|
| 875 |
+
presetSelect.appendChild(option);
|
| 876 |
+
});
|
| 877 |
+
}
|
| 878 |
+
|
| 879 |
+
// Main GUI panel
|
| 880 |
+
const mainGui = new dat.GUI();
|
| 881 |
+
|
| 882 |
+
// FOG PARAMS
|
| 883 |
+
const fogParams = {
|
| 884 |
+
enabled: true,
|
| 885 |
+
color: '#000000',
|
| 886 |
+
near: 2.7,
|
| 887 |
+
far: 3.7,
|
| 888 |
+
};
|
| 889 |
+
|
| 890 |
+
// After fog update
|
| 891 |
+
function updateFog() {
|
| 892 |
+
if (!fogParams.enabled) {
|
| 893 |
+
scene.fog = null;
|
| 894 |
+
} else {
|
| 895 |
+
const color = new THREE.Color(fogParams.color);
|
| 896 |
+
scene.fog = new THREE.Fog(color, fogParams.near, fogParams.far);
|
| 897 |
+
|
| 898 |
+
}
|
| 899 |
+
renderer.render(scene, camera); // Překreslení scény
|
| 900 |
+
}
|
| 901 |
+
|
| 902 |
+
// Fog init
|
| 903 |
+
if (fogParams.enabled) {
|
| 904 |
+
updateFog();
|
| 905 |
+
}
|
| 906 |
+
|
| 907 |
+
// Adding to main GUI
|
| 908 |
+
mainGui.add(fogParams, 'enabled').name('Fog Enabled').onChange(updateFog);
|
| 909 |
+
mainGui.addColor(fogParams, 'color').name('Fog Color').onChange(updateFog);
|
| 910 |
+
mainGui.add(fogParams, 'near', 0.1, 5, 0.1).name('Fog Near').onChange(updateFog);
|
| 911 |
+
mainGui.add(fogParams, 'far', 0.1, 5, 0.1).name('Fog Far').onChange(updateFog);
|
| 912 |
+
|
| 913 |
+
// Main GUI particleCount
|
| 914 |
+
mainGui.add({
|
| 915 |
+
globalParticleCount: 20000
|
| 916 |
+
}, 'globalParticleCount', 1000, 100000).step(1000).onChange(value => {
|
| 917 |
+
spheres.forEach((sphere, index) => {
|
| 918 |
+
sphere.params.particleCount = value;
|
| 919 |
+
const {
|
| 920 |
+
newPositions,
|
| 921 |
+
newColors,
|
| 922 |
+
newVelocities,
|
| 923 |
+
newBasePositions,
|
| 924 |
+
newLifetimes,
|
| 925 |
+
newMaxLifetimes,
|
| 926 |
+
newBeatEffects
|
| 927 |
+
} = reinitializeParticlesForSphere(
|
| 928 |
+
sphere, sphere.params, sphere.geometry
|
| 929 |
+
);
|
| 930 |
+
|
| 931 |
+
sphere.positions = newPositions;
|
| 932 |
+
sphere.colors = newColors;
|
| 933 |
+
sphere.velocities = newVelocities;
|
| 934 |
+
sphere.basePositions = newBasePositions;
|
| 935 |
+
sphere.lifetimes = newLifetimes;
|
| 936 |
+
sphere.maxLifetimes = newMaxLifetimes;
|
| 937 |
+
sphere.beatEffects = newBeatEffects;
|
| 938 |
+
|
| 939 |
+
sphere.geometry.attributes.position.needsUpdate = true;
|
| 940 |
+
sphere.geometry.attributes.color.needsUpdate = true;
|
| 941 |
+
|
| 942 |
+
const sphereFolder = mainGui.__folders[`Sphere ${index + 1}`];
|
| 943 |
+
if (sphereFolder) {
|
| 944 |
+
const particleCountController = sphereFolder.__controllers.find(controller => controller.property === 'particleCount');
|
| 945 |
+
if (particleCountController) {
|
| 946 |
+
particleCountController.updateDisplay();
|
| 947 |
+
}
|
| 948 |
+
}
|
| 949 |
+
});
|
| 950 |
+
});
|
| 951 |
+
|
| 952 |
+
const spheres = [];
|
| 953 |
+
|
| 954 |
+
function createSphereVisualization(index) {
|
| 955 |
+
|
| 956 |
+
// Spheres default frequencies
|
| 957 |
+
const defaultFrequencies = [
|
| 958 |
+
{ minFrequency: 20, maxFrequency: 80 }, // Sub-bass
|
| 959 |
+
{ minFrequency: 120, maxFrequency: 250 }, // Bass
|
| 960 |
+
{ minFrequency: 250, maxFrequency: 800 }, // Mid
|
| 961 |
+
{ minFrequency: 1000, maxFrequency: 4000 }, // High mid
|
| 962 |
+
{ minFrequency: 5000, maxFrequency: 10000 } // High
|
| 963 |
+
];
|
| 964 |
+
|
| 965 |
+
const sphereParams = {
|
| 966 |
+
enabled: index === 0,
|
| 967 |
+
sphereRadius: 1.0,
|
| 968 |
+
innerSphereRadius: 0.25,
|
| 969 |
+
rotationSpeed: 0.001,
|
| 970 |
+
rotationSpeedMin: 0,
|
| 971 |
+
rotationSpeedMax: 0.065,
|
| 972 |
+
rotationSmoothness: 0.3,
|
| 973 |
+
particleCount: 20000,
|
| 974 |
+
particleSize: 0.003,
|
| 975 |
+
particleLifetime: 3.0,
|
| 976 |
+
minFrequency: defaultFrequencies[index]?.minFrequency || 0,
|
| 977 |
+
maxFrequency: defaultFrequencies[index]?.maxFrequency || 22050,
|
| 978 |
+
minFrequencyBeat: defaultFrequencies[index]?.minFrequency || 0,
|
| 979 |
+
maxFrequencyBeat: defaultFrequencies[index]?.maxFrequency || 22050,
|
| 980 |
+
noiseScale: 4.0,
|
| 981 |
+
dynamicNoiseScale: true,
|
| 982 |
+
minNoiseScale: 0.5,
|
| 983 |
+
maxNoiseScale: 5.0,
|
| 984 |
+
noiseStep: 0.2,
|
| 985 |
+
noiseSpeed: 0.1,
|
| 986 |
+
turbulenceStrength: 0.005,
|
| 987 |
+
colorStart: '#ff3366',
|
| 988 |
+
colorEnd: '#3366ff',
|
| 989 |
+
volumeChangeThreshold: 0.1,
|
| 990 |
+
peakSensitivity: 1.1,
|
| 991 |
+
beatThreshold: 200,
|
| 992 |
+
baseWaveStrength: 20.0,
|
| 993 |
+
beatStrength: 0.01,
|
| 994 |
+
gainMultiplier: 1
|
| 995 |
+
};
|
| 996 |
+
|
| 997 |
+
const sphereGeometry = new THREE.BufferGeometry();
|
| 998 |
+
const spherePositions = new Float32Array(sphereParams.particleCount * 3);
|
| 999 |
+
const sphereColors = new Float32Array(sphereParams.particleCount * 3);
|
| 1000 |
+
const velocities = new Float32Array(sphereParams.particleCount * 3);
|
| 1001 |
+
const basePositions = new Float32Array(sphereParams.particleCount * 3);
|
| 1002 |
+
const lifetimes = new Float32Array(sphereParams.particleCount);
|
| 1003 |
+
const maxLifetimes = new Float32Array(sphereParams.particleCount);
|
| 1004 |
+
const beatEffects = new Float32Array(sphereParams.particleCount);
|
| 1005 |
+
|
| 1006 |
+
// Init particles
|
| 1007 |
+
for (let i = 0; i < sphereParams.particleCount; i++) {
|
| 1008 |
+
const i3 = i * 3;
|
| 1009 |
+
const radius = THREE.MathUtils.lerp(0, sphereParams.sphereRadius, sphereParams.innerSphereRadius);
|
| 1010 |
+
const theta = Math.random() * Math.PI * 2;
|
| 1011 |
+
const phi = Math.acos(2 * Math.random() - 1);
|
| 1012 |
+
const r = Math.cbrt(Math.random()) * radius;
|
| 1013 |
+
|
| 1014 |
+
const x = r * Math.sin(phi) * Math.cos(theta);
|
| 1015 |
+
const y = r * Math.sin(phi) * Math.sin(theta);
|
| 1016 |
+
const z = r * Math.cos(phi);
|
| 1017 |
+
|
| 1018 |
+
spherePositions[i3] = x;
|
| 1019 |
+
spherePositions[i3 + 1] = y;
|
| 1020 |
+
spherePositions[i3 + 2] = z;
|
| 1021 |
+
|
| 1022 |
+
basePositions[i3] = x;
|
| 1023 |
+
basePositions[i3 + 1] = y;
|
| 1024 |
+
basePositions[i3 + 2] = z;
|
| 1025 |
+
|
| 1026 |
+
velocities[i3] = 0;
|
| 1027 |
+
velocities[i3 + 1] = 0;
|
| 1028 |
+
velocities[i3 + 2] = 0;
|
| 1029 |
+
|
| 1030 |
+
const lt = Math.random() * sphereParams.particleLifetime;
|
| 1031 |
+
lifetimes[i] = lt;
|
| 1032 |
+
maxLifetimes[i] = lt;
|
| 1033 |
+
|
| 1034 |
+
beatEffects[i] = 0;
|
| 1035 |
+
}
|
| 1036 |
+
|
| 1037 |
+
sphereGeometry.setAttribute('position', new THREE.BufferAttribute(spherePositions, 3));
|
| 1038 |
+
sphereGeometry.setAttribute('color', new THREE.BufferAttribute(sphereColors, 3));
|
| 1039 |
+
|
| 1040 |
+
const sphereMaterial = new THREE.PointsMaterial({
|
| 1041 |
+
size: sphereParams.particleSize,
|
| 1042 |
+
vertexColors: true,
|
| 1043 |
+
transparent: true,
|
| 1044 |
+
opacity: 0.8,
|
| 1045 |
+
blending: THREE.AdditiveBlending,
|
| 1046 |
+
fog: true
|
| 1047 |
+
});
|
| 1048 |
+
|
| 1049 |
+
const sphereParticleSystem = new THREE.Points(sphereGeometry, sphereMaterial);
|
| 1050 |
+
scene.add(sphereParticleSystem);
|
| 1051 |
+
|
| 1052 |
+
// Sphere visibility `enabled`
|
| 1053 |
+
sphereParticleSystem.visible = sphereParams.enabled;
|
| 1054 |
+
|
| 1055 |
+
const sphere = {
|
| 1056 |
+
index: index,
|
| 1057 |
+
params: sphereParams,
|
| 1058 |
+
geometry: sphereGeometry,
|
| 1059 |
+
colors: sphereColors,
|
| 1060 |
+
material: sphereMaterial,
|
| 1061 |
+
particleSystem: sphereParticleSystem,
|
| 1062 |
+
positions: spherePositions,
|
| 1063 |
+
velocities: velocities,
|
| 1064 |
+
basePositions: basePositions,
|
| 1065 |
+
lifetimes: lifetimes,
|
| 1066 |
+
maxLifetimes: maxLifetimes,
|
| 1067 |
+
beatEffects: beatEffects,
|
| 1068 |
+
lastNoiseScale: sphereParams.noiseScale,
|
| 1069 |
+
lastValidVolume: 0,
|
| 1070 |
+
lastRotationSpeed: 0
|
| 1071 |
+
};
|
| 1072 |
+
|
| 1073 |
+
sphere.peakDetection = {
|
| 1074 |
+
energyHistory: [],
|
| 1075 |
+
historyLength: 30,
|
| 1076 |
+
lastPeakTime: 0,
|
| 1077 |
+
minTimeBetweenPeaks: 200
|
| 1078 |
+
};
|
| 1079 |
+
|
| 1080 |
+
// Colors update
|
| 1081 |
+
updateColorsForSphere(sphereParams, sphereGeometry, sphereColors);
|
| 1082 |
+
|
| 1083 |
+
// GUI folder
|
| 1084 |
+
const sphereFolder = mainGui.addFolder('Sphere ' + (index + 1));
|
| 1085 |
+
|
| 1086 |
+
sphereFolder.add(sphere.params, 'particleCount', 1000, 100000).step(1000)
|
| 1087 |
+
.onChange(() => {
|
| 1088 |
+
const {
|
| 1089 |
+
newPositions,
|
| 1090 |
+
newColors,
|
| 1091 |
+
newVelocities,
|
| 1092 |
+
newBasePositions,
|
| 1093 |
+
newLifetimes,
|
| 1094 |
+
newMaxLifetimes,
|
| 1095 |
+
newBeatEffects
|
| 1096 |
+
} = reinitializeParticlesForSphere(
|
| 1097 |
+
sphere, sphere.params, sphere.geometry
|
| 1098 |
+
);
|
| 1099 |
+
|
| 1100 |
+
sphere.positions = newPositions;
|
| 1101 |
+
sphere.colors = newColors;
|
| 1102 |
+
sphere.velocities = newVelocities;
|
| 1103 |
+
sphere.basePositions = newBasePositions;
|
| 1104 |
+
sphere.lifetimes = newLifetimes;
|
| 1105 |
+
sphere.maxLifetimes = newMaxLifetimes;
|
| 1106 |
+
sphere.beatEffects = newBeatEffects;
|
| 1107 |
+
|
| 1108 |
+
sphere.geometry.attributes.position.needsUpdate = true;
|
| 1109 |
+
sphere.geometry.attributes.color.needsUpdate = true;
|
| 1110 |
+
});
|
| 1111 |
+
|
| 1112 |
+
sphereFolder.add(sphere.params, 'particleSize', 0.001, 0.01).step(0.001)
|
| 1113 |
+
.onChange(value => {
|
| 1114 |
+
sphere.material.size = value;
|
| 1115 |
+
});
|
| 1116 |
+
|
| 1117 |
+
if (index === 0) {
|
| 1118 |
+
sphereFolder.add({ copyToOthers: () => {
|
| 1119 |
+
for (let i = 1; i < spheres.length; i++) {
|
| 1120 |
+
Object.assign(spheres[i].params, JSON.parse(JSON.stringify(sphere.params)));
|
| 1121 |
+
|
| 1122 |
+
const {
|
| 1123 |
+
newPositions,
|
| 1124 |
+
newColors,
|
| 1125 |
+
newVelocities,
|
| 1126 |
+
newBasePositions,
|
| 1127 |
+
newLifetimes,
|
| 1128 |
+
newMaxLifetimes,
|
| 1129 |
+
newBeatEffects
|
| 1130 |
+
} = reinitializeParticlesForSphere(
|
| 1131 |
+
spheres[i], spheres[i].params, spheres[i].geometry
|
| 1132 |
+
);
|
| 1133 |
+
|
| 1134 |
+
spheres[i].positions = newPositions;
|
| 1135 |
+
spheres[i].colors = newColors;
|
| 1136 |
+
spheres[i].velocities = newVelocities;
|
| 1137 |
+
spheres[i].basePositions = newBasePositions;
|
| 1138 |
+
spheres[i].lifetimes = newLifetimes;
|
| 1139 |
+
spheres[i].maxLifetimes = newMaxLifetimes;
|
| 1140 |
+
spheres[i].beatEffects = newBeatEffects;
|
| 1141 |
+
|
| 1142 |
+
spheres[i].geometry.attributes.position.needsUpdate = true;
|
| 1143 |
+
spheres[i].geometry.attributes.color.needsUpdate = true;
|
| 1144 |
+
|
| 1145 |
+
|
| 1146 |
+
spheres[i].particleSystem.visible = spheres[i].params.enabled;
|
| 1147 |
+
|
| 1148 |
+
const targetFolder = mainGui.__folders[`Sphere ${i + 1}`];
|
| 1149 |
+
if (targetFolder) {
|
| 1150 |
+
targetFolder.__controllers.forEach(controller => controller.updateDisplay());
|
| 1151 |
+
}
|
| 1152 |
+
}
|
| 1153 |
+
mainGui.updateDisplay();
|
| 1154 |
+
console.log('Parameters copied from Sphere 1 to Spheres 2-5.');
|
| 1155 |
+
}}, 'copyToOthers').name('Copy to Spheres 2-5');
|
| 1156 |
+
}
|
| 1157 |
+
sphereFolder.add(sphere.params, 'particleLifetime', 1, 20).step(1);
|
| 1158 |
+
|
| 1159 |
+
sphereFolder.add(sphere.params, 'sphereRadius', 0.05, 3.0).step(0.05);
|
| 1160 |
+
sphereFolder.add(sphere.params, 'innerSphereRadius', 0, 1).step(0.01)
|
| 1161 |
+
.onChange(() => {
|
| 1162 |
+
const {
|
| 1163 |
+
newPositions,
|
| 1164 |
+
newColors,
|
| 1165 |
+
newVelocities,
|
| 1166 |
+
newBasePositions,
|
| 1167 |
+
newLifetimes,
|
| 1168 |
+
newMaxLifetimes,
|
| 1169 |
+
newBeatEffects
|
| 1170 |
+
} = reinitializeParticlesForSphere(sphere, sphere.params, sphere.geometry);
|
| 1171 |
+
|
| 1172 |
+
sphere.positions = newPositions;
|
| 1173 |
+
sphere.colors = newColors;
|
| 1174 |
+
sphere.velocities = newVelocities;
|
| 1175 |
+
sphere.basePositions = newBasePositions;
|
| 1176 |
+
sphere.lifetimes = newLifetimes;
|
| 1177 |
+
sphere.maxLifetimes = newMaxLifetimes;
|
| 1178 |
+
sphere.beatEffects = newBeatEffects;
|
| 1179 |
+
|
| 1180 |
+
sphere.geometry.attributes.position.needsUpdate = true;
|
| 1181 |
+
sphere.geometry.attributes.color.needsUpdate = true;
|
| 1182 |
+
});
|
| 1183 |
+
|
| 1184 |
+
sphereFolder.add(sphere.params, 'rotationSpeedMin', 0, 0.02).step(0.001);
|
| 1185 |
+
sphereFolder.add(sphere.params, 'rotationSpeedMax', 0, 0.1).step(0.001);
|
| 1186 |
+
sphereFolder.add(sphere.params, 'rotationSmoothness', 0.01, 1).step(0.01);
|
| 1187 |
+
sphereFolder.add(sphere.params, 'volumeChangeThreshold', 0.01, 0.2).step(0.01);
|
| 1188 |
+
|
| 1189 |
+
sphereFolder.add(sphereParams, 'minFrequency', 0, 22050).step(1).name('Min Frequency (Hz)')
|
| 1190 |
+
.onChange(value => sphereParams.minFrequency = value);
|
| 1191 |
+
sphereFolder.add(sphereParams, 'maxFrequency', 0, 22050).step(1).name('Max Frequency (Hz)')
|
| 1192 |
+
.onChange(value => sphereParams.maxFrequency = value);
|
| 1193 |
+
|
| 1194 |
+
// GUI defaults
|
| 1195 |
+
const minFreqController = sphereFolder.__controllers.find(c => c.property === 'minFrequency');
|
| 1196 |
+
const maxFreqController = sphereFolder.__controllers.find(c => c.property === 'maxFrequency');
|
| 1197 |
+
if (minFreqController) minFreqController.setValue(sphereParams.minFrequency);
|
| 1198 |
+
if (maxFreqController) maxFreqController.setValue(sphereParams.maxFrequency);
|
| 1199 |
+
|
| 1200 |
+
sphereFolder.add(sphere.params, 'noiseScale', 0.1, 10.0).step(0.1);
|
| 1201 |
+
sphereFolder.add(sphere.params, 'minNoiseScale', 0.0, 10.0).step(0.1).name('Min NoiseScale')
|
| 1202 |
+
.onChange(() => {
|
| 1203 |
+
if (sphere.params.minNoiseScale > sphere.params.maxNoiseScale) {
|
| 1204 |
+
sphere.params.minNoiseScale = sphere.params.maxNoiseScale;
|
| 1205 |
+
}
|
| 1206 |
+
updateNoiseStep(sphere.params);
|
| 1207 |
+
});
|
| 1208 |
+
sphereFolder.add(sphere.params, 'maxNoiseScale', 0.0, 10.0).step(0.1).name('Max NoiseScale')
|
| 1209 |
+
.onChange(() => {
|
| 1210 |
+
if (sphere.params.maxNoiseScale < sphere.params.minNoiseScale) {
|
| 1211 |
+
sphere.params.maxNoiseScale = sphere.params.minNoiseScale;
|
| 1212 |
+
}
|
| 1213 |
+
updateNoiseStep(sphere.params);
|
| 1214 |
+
});
|
| 1215 |
+
sphereFolder.add(sphere.params, 'noiseStep', 0.1, 5.0).step(0.1).name('Noise Step')
|
| 1216 |
+
.onChange(() => {
|
| 1217 |
+
updateNoiseStep(sphere.params);
|
| 1218 |
+
});
|
| 1219 |
+
function updateNoiseStep(params) {
|
| 1220 |
+
const range = params.maxNoiseScale - params.minNoiseScale;
|
| 1221 |
+
if (params.noiseStep > range) {
|
| 1222 |
+
params.noiseStep = range / 2;
|
| 1223 |
+
}
|
| 1224 |
+
}
|
| 1225 |
+
sphereFolder.add(sphere.params, 'noiseSpeed', 0, 1.0).step(0.01);
|
| 1226 |
+
|
| 1227 |
+
sphereFolder.add(sphere.params, 'peakSensitivity', 1.01, 2).step(0.01);
|
| 1228 |
+
sphereFolder.add(sphere.peakDetection, 'historyLength', 10, 1200).step(1).name('History Length');
|
| 1229 |
+
sphereFolder.add(sphere.peakDetection, 'minTimeBetweenPeaks', 50, 5000).step(10).name('Min Time Between Peaks');
|
| 1230 |
+
|
| 1231 |
+
sphereFolder.add(sphere.params, 'turbulenceStrength', 0, 0.03).step(0.0001);
|
| 1232 |
+
sphereFolder.addColor(sphere.params, 'colorStart')
|
| 1233 |
+
.onChange(() => updateColorsForSphere(sphere.params, sphere.geometry, sphere.colors));
|
| 1234 |
+
sphereFolder.addColor(sphere.params, 'colorEnd')
|
| 1235 |
+
.onChange(() => updateColorsForSphere(sphere.params, sphere.geometry, sphere.colors));
|
| 1236 |
+
|
| 1237 |
+
sphereFolder.add(sphereParams, 'minFrequencyBeat', 0, 22050).step(1).name('Min Freq Beat (Hz)')
|
| 1238 |
+
.onChange(value => sphereParams.minFrequencyBeat = value);
|
| 1239 |
+
sphereFolder.add(sphereParams, 'maxFrequencyBeat', 0, 22050).step(1).name('Max Freq Beat (Hz)')
|
| 1240 |
+
.onChange(value => sphereParams.maxFrequencyBeat = value);
|
| 1241 |
+
|
| 1242 |
+
sphereFolder.add(sphere.params, 'beatThreshold', 50, 255).step(1);
|
| 1243 |
+
sphereFolder.add(sphere.params, 'beatStrength', 0, 0.05).step(0.001);
|
| 1244 |
+
sphereFolder.add(sphere.params, 'gainMultiplier', 1.0, 3.0).step(0.1);
|
| 1245 |
+
sphereFolder.add(sphere.params, 'dynamicNoiseScale');
|
| 1246 |
+
sphereFolder.add(sphere.params, 'enabled').onChange(value => {
|
| 1247 |
+
sphere.particleSystem.visible = value;
|
| 1248 |
+
});
|
| 1249 |
+
|
| 1250 |
+
sphereFolder.close();
|
| 1251 |
+
|
| 1252 |
+
return sphere;
|
| 1253 |
+
}
|
| 1254 |
+
|
| 1255 |
+
for (let i = 0; i < 5; i++) {
|
| 1256 |
+
const sphereVis = createSphereVisualization(i);
|
| 1257 |
+
spheres.push(sphereVis);
|
| 1258 |
+
}
|
| 1259 |
+
|
| 1260 |
+
// Saving defaults spheres
|
| 1261 |
+
spheres.forEach(sphere => {
|
| 1262 |
+
defaultParams.push(JSON.parse(JSON.stringify(sphere.params)));
|
| 1263 |
+
});
|
| 1264 |
+
|
| 1265 |
+
// Init presets
|
| 1266 |
+
updatePresetOptions();
|
| 1267 |
+
|
| 1268 |
+
// Only sphere 1 allowed
|
| 1269 |
+
spheres.forEach((sphere, index) => {
|
| 1270 |
+
if (index !== 0) {
|
| 1271 |
+
sphere.params.enabled = false;
|
| 1272 |
+
sphere.particleSystem.visible = false;
|
| 1273 |
+
}
|
| 1274 |
+
});
|
| 1275 |
+
|
| 1276 |
+
function getSmoothVolume(params, lastValidVolume, volumeChangeThreshold) {
|
| 1277 |
+
if (!analyser) return { volume: 0, shouldUpdate: false };
|
| 1278 |
+
|
| 1279 |
+
const bufferLength = analyser.frequencyBinCount;
|
| 1280 |
+
const dataArray = new Uint8Array(bufferLength);
|
| 1281 |
+
analyser.getByteFrequencyData(dataArray);
|
| 1282 |
+
|
| 1283 |
+
let sum = 0;
|
| 1284 |
+
for (let i = 0; i < bufferLength; i++) {
|
| 1285 |
+
sum += dataArray[i];
|
| 1286 |
+
}
|
| 1287 |
+
const average = sum / bufferLength;
|
| 1288 |
+
const normalizedVolume = average / 255;
|
| 1289 |
+
|
| 1290 |
+
let shouldUpdate = true;
|
| 1291 |
+
if (lastValidVolume === 0) {
|
| 1292 |
+
lastValidVolume = normalizedVolume;
|
| 1293 |
+
} else {
|
| 1294 |
+
const change = Math.abs(normalizedVolume - lastValidVolume);
|
| 1295 |
+
if (change <= volumeChangeThreshold) {
|
| 1296 |
+
lastValidVolume = normalizedVolume;
|
| 1297 |
+
} else {
|
| 1298 |
+
shouldUpdate = false;
|
| 1299 |
+
}
|
| 1300 |
+
}
|
| 1301 |
+
|
| 1302 |
+
return { volume: lastValidVolume, shouldUpdate };
|
| 1303 |
+
}
|
| 1304 |
+
|
| 1305 |
+
let lastTime = 0;
|
| 1306 |
+
function animate(currentTime) {
|
| 1307 |
+
requestAnimationFrame(animate);
|
| 1308 |
+
|
| 1309 |
+
const deltaTime = lastTime ? (currentTime - lastTime) / 1000 : 0;
|
| 1310 |
+
lastTime = currentTime;
|
| 1311 |
+
|
| 1312 |
+
// Beat wave update
|
| 1313 |
+
beatManager.update(deltaTime);
|
| 1314 |
+
|
| 1315 |
+
// Sphere update
|
| 1316 |
+
spheres.forEach(sphere => {
|
| 1317 |
+
if (!sphere.params.enabled) return;
|
| 1318 |
+
|
| 1319 |
+
const audioData = getAudioData(sphere);
|
| 1320 |
+
|
| 1321 |
+
if (audioData.peakDetected) {
|
| 1322 |
+
if (sphere.params.dynamicNoiseScale) {
|
| 1323 |
+
sphere.params.noiseScale = generateNewNoiseScale(
|
| 1324 |
+
sphere.params,
|
| 1325 |
+
sphere.lastNoiseScale
|
| 1326 |
+
);
|
| 1327 |
+
sphere.lastNoiseScale = sphere.params.noiseScale;
|
| 1328 |
+
}
|
| 1329 |
+
}
|
| 1330 |
+
|
| 1331 |
+
const { params, geometry, positions, velocities, basePositions, lifetimes, maxLifetimes, beatEffects } = sphere;
|
| 1332 |
+
|
| 1333 |
+
// Beat detection sphere
|
| 1334 |
+
const beatDetected = audioData.rangeEnergyBeat > params.beatThreshold;
|
| 1335 |
+
|
| 1336 |
+
// Beat wave sphere
|
| 1337 |
+
if (beatDetected && !beatManager.isWaveActive && params.beatStrength > 0) {
|
| 1338 |
+
beatManager.triggerWave(audioData.rangeEnergyBeat);
|
| 1339 |
+
}
|
| 1340 |
+
|
| 1341 |
+
// Update particles
|
| 1342 |
+
const pc = params.particleCount;
|
| 1343 |
+
for (let i = 0; i < pc; i++) {
|
| 1344 |
+
const i3 = i * 3;
|
| 1345 |
+
|
| 1346 |
+
let x = positions[i3];
|
| 1347 |
+
let y = positions[i3 + 1];
|
| 1348 |
+
let z = positions[i3 + 2];
|
| 1349 |
+
|
| 1350 |
+
let vx = velocities[i3];
|
| 1351 |
+
let vy = velocities[i3 + 1];
|
| 1352 |
+
let vz = velocities[i3 + 2];
|
| 1353 |
+
|
| 1354 |
+
let lt = lifetimes[i];
|
| 1355 |
+
let be = beatEffects[i];
|
| 1356 |
+
|
| 1357 |
+
// Update lifetime
|
| 1358 |
+
lt -= deltaTime;
|
| 1359 |
+
|
| 1360 |
+
// Noise calc
|
| 1361 |
+
const ns = params.noiseScale;
|
| 1362 |
+
const speed = params.noiseSpeed;
|
| 1363 |
+
const timeFactor = currentTime * 0.001;
|
| 1364 |
+
const noiseX = noise.noise3D(x * ns + timeFactor * speed, y * ns, z * ns);
|
| 1365 |
+
const noiseY = noise.noise3D(x * ns, y * ns + timeFactor * speed, z * ns);
|
| 1366 |
+
const noiseZ = noise.noise3D(x * ns, y * ns, z * ns + timeFactor * speed);
|
| 1367 |
+
|
| 1368 |
+
vx += noiseX * params.turbulenceStrength;
|
| 1369 |
+
vy += noiseY * params.turbulenceStrength;
|
| 1370 |
+
vz += noiseZ * params.turbulenceStrength;
|
| 1371 |
+
|
| 1372 |
+
// Beat effect
|
| 1373 |
+
if (beatDetected) {
|
| 1374 |
+
be = 1.0;
|
| 1375 |
+
}
|
| 1376 |
+
be *= 0.95;
|
| 1377 |
+
if (be > 0.01) {
|
| 1378 |
+
// směr z centra
|
| 1379 |
+
const dist = Math.sqrt(x*x + y*y + z*z);
|
| 1380 |
+
if (dist > 0) {
|
| 1381 |
+
const dx = x / dist;
|
| 1382 |
+
const dy = y / dist;
|
| 1383 |
+
const dz = z / dist;
|
| 1384 |
+
|
| 1385 |
+
const beatForce = be * params.beatStrength;
|
| 1386 |
+
vx += dx * beatForce;
|
| 1387 |
+
vy += dy * beatForce;
|
| 1388 |
+
vz += dz * beatForce;
|
| 1389 |
+
}
|
| 1390 |
+
}
|
| 1391 |
+
|
| 1392 |
+
// Update positions
|
| 1393 |
+
x += vx;
|
| 1394 |
+
y += vy;
|
| 1395 |
+
z += vz;
|
| 1396 |
+
|
| 1397 |
+
// Slowing speed
|
| 1398 |
+
vx *= 0.98;
|
| 1399 |
+
vy *= 0.98;
|
| 1400 |
+
vz *= 0.98;
|
| 1401 |
+
|
| 1402 |
+
// Radius control
|
| 1403 |
+
const dist = Math.sqrt(x*x + y*y + z*z);
|
| 1404 |
+
if (dist > params.sphereRadius) {
|
| 1405 |
+
const overflow = dist - params.sphereRadius;
|
| 1406 |
+
const pullback = overflow * 0.1;
|
| 1407 |
+
if (dist > 0) {
|
| 1408 |
+
const dx = x / dist;
|
| 1409 |
+
const dy = y / dist;
|
| 1410 |
+
const dz = z / dist;
|
| 1411 |
+
x -= dx * pullback;
|
| 1412 |
+
y -= dy * pullback;
|
| 1413 |
+
z -= dz * pullback;
|
| 1414 |
+
}
|
| 1415 |
+
vx *= 0.9;
|
| 1416 |
+
vy *= 0.9;
|
| 1417 |
+
vz *= 0.9;
|
| 1418 |
+
}
|
| 1419 |
+
|
| 1420 |
+
// Reset of dead particles
|
| 1421 |
+
if (lt <= 0) {
|
| 1422 |
+
const radius = THREE.MathUtils.lerp(0, params.sphereRadius, params.innerSphereRadius);
|
| 1423 |
+
const theta = Math.random() * Math.PI * 2;
|
| 1424 |
+
const phi = Math.acos(2 * Math.random() - 1);
|
| 1425 |
+
const rr = Math.cbrt(Math.random()) * radius;
|
| 1426 |
+
|
| 1427 |
+
x = rr * Math.sin(phi) * Math.cos(theta);
|
| 1428 |
+
y = rr * Math.sin(phi) * Math.sin(theta);
|
| 1429 |
+
z = rr * Math.cos(phi);
|
| 1430 |
+
|
| 1431 |
+
vx = 0;
|
| 1432 |
+
vy = 0;
|
| 1433 |
+
vz = 0;
|
| 1434 |
+
|
| 1435 |
+
const newLt = Math.random() * params.particleLifetime;
|
| 1436 |
+
lt = newLt;
|
| 1437 |
+
maxLifetimes[i] = newLt;
|
| 1438 |
+
be = 0;
|
| 1439 |
+
|
| 1440 |
+
basePositions[i3] = x;
|
| 1441 |
+
basePositions[i3 + 1] = y;
|
| 1442 |
+
basePositions[i3 + 2] = z;
|
| 1443 |
+
}
|
| 1444 |
+
|
| 1445 |
+
positions[i3] = x;
|
| 1446 |
+
positions[i3 + 1] = y;
|
| 1447 |
+
positions[i3 + 2] = z;
|
| 1448 |
+
|
| 1449 |
+
velocities[i3] = vx;
|
| 1450 |
+
velocities[i3 + 1] = vy;
|
| 1451 |
+
velocities[i3 + 2] = vz;
|
| 1452 |
+
|
| 1453 |
+
lifetimes[i] = lt;
|
| 1454 |
+
beatEffects[i] = be;
|
| 1455 |
+
}
|
| 1456 |
+
|
| 1457 |
+
geometry.attributes.position.needsUpdate = true;
|
| 1458 |
+
|
| 1459 |
+
// Dyn rotation
|
| 1460 |
+
const { volume: smoothVolume, shouldUpdate } = getSmoothVolume(
|
| 1461 |
+
params,
|
| 1462 |
+
sphere.lastValidVolume,
|
| 1463 |
+
params.volumeChangeThreshold
|
| 1464 |
+
);
|
| 1465 |
+
|
| 1466 |
+
if (shouldUpdate) {
|
| 1467 |
+
const targetRotationSpeed = THREE.MathUtils.lerp(
|
| 1468 |
+
params.rotationSpeedMin,
|
| 1469 |
+
params.rotationSpeedMax,
|
| 1470 |
+
smoothVolume
|
| 1471 |
+
);
|
| 1472 |
+
sphere.lastRotationSpeed = params.rotationSpeed +
|
| 1473 |
+
(targetRotationSpeed - params.rotationSpeed) *
|
| 1474 |
+
params.rotationSmoothness;
|
| 1475 |
+
}
|
| 1476 |
+
|
| 1477 |
+
sphere.particleSystem.rotation.y += sphere.lastRotationSpeed;
|
| 1478 |
+
|
| 1479 |
+
// Saving volume
|
| 1480 |
+
if (shouldUpdate) sphere.lastValidVolume = smoothVolume;
|
| 1481 |
+
});
|
| 1482 |
+
|
| 1483 |
+
renderer.render(scene, camera);
|
| 1484 |
+
updateTimeline();
|
| 1485 |
+
}
|
| 1486 |
+
|
| 1487 |
+
window.addEventListener('resize', () => {
|
| 1488 |
+
renderer.setSize(window.innerWidth, window.innerHeight);
|
| 1489 |
+
camera.aspect = window.innerWidth / window.innerHeight;
|
| 1490 |
+
camera.updateProjectionMatrix();
|
| 1491 |
+
});
|
| 1492 |
+
|
| 1493 |
+
let controlsVisible = true; // Stav viditelnosti
|
| 1494 |
+
|
| 1495 |
+
// Switching control elements
|
| 1496 |
+
function toggleControlsVisibility() {
|
| 1497 |
+
controlsVisible = !controlsVisible;
|
| 1498 |
+
|
| 1499 |
+
const allControls = document.querySelectorAll(
|
| 1500 |
+
'.controls-container, .dg.main, #audioControls, #presetContainer, #songSelect, #playPause, input[type="range"], button, select, input[type="text"]'
|
| 1501 |
+
);
|
| 1502 |
+
|
| 1503 |
+
allControls.forEach(control => {
|
| 1504 |
+
control.style.display = controlsVisible ? 'block' : 'none';
|
| 1505 |
+
});
|
| 1506 |
+
}
|
| 1507 |
+
|
| 1508 |
+
// Event listener on click
|
| 1509 |
+
document.addEventListener('click', (event) => {
|
| 1510 |
+
const clickedElement = event.target;
|
| 1511 |
+
if (!clickedElement.closest('.controls-container') &&
|
| 1512 |
+
!clickedElement.closest('.dg.main') &&
|
| 1513 |
+
!clickedElement.closest('#audioControls') &&
|
| 1514 |
+
!clickedElement.closest('#presetContainer') &&
|
| 1515 |
+
!clickedElement.closest('#songSelect') &&
|
| 1516 |
+
!clickedElement.closest('#playPause') &&
|
| 1517 |
+
!clickedElement.closest('input[type="range"]') &&
|
| 1518 |
+
!clickedElement.closest('button') &&
|
| 1519 |
+
!clickedElement.closest('select') &&
|
| 1520 |
+
!clickedElement.closest('input[type="text"]')) {
|
| 1521 |
+
toggleControlsVisibility();
|
| 1522 |
+
}
|
| 1523 |
+
});
|
| 1524 |
+
|
| 1525 |
+
document.querySelectorAll('.controls-container, .dg.main, #audioControls, #presetContainer').forEach(control => {
|
| 1526 |
+
control.style.position = 'absolute';
|
| 1527 |
+
});
|
| 1528 |
+
|
| 1529 |
+
console.log('Starting animation');
|
| 1530 |
+
initAudio();
|
| 1531 |
+
animate(0);
|
package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "living-nexus",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"description": "An interactive audio-visual artifact - Living Nexus",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "vite",
|
| 8 |
+
"build": "tsc && vite build",
|
| 9 |
+
"preview": "vite preview",
|
| 10 |
+
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
|
| 11 |
+
},
|
| 12 |
+
"dependencies": {
|
| 13 |
+
"@types/three": "^0.177.0",
|
| 14 |
+
"react": "^18.2.0",
|
| 15 |
+
"react-dom": "^18.2.0",
|
| 16 |
+
"three": "^0.177.0",
|
| 17 |
+
"zustand": "^4.4.1"
|
| 18 |
+
},
|
| 19 |
+
"devDependencies": {
|
| 20 |
+
"@types/react": "^18.2.15",
|
| 21 |
+
"@types/react-dom": "^18.2.7",
|
| 22 |
+
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
| 23 |
+
"@typescript-eslint/parser": "^6.0.0",
|
| 24 |
+
"@vitejs/plugin-react": "^4.0.3",
|
| 25 |
+
"eslint": "^8.45.0",
|
| 26 |
+
"eslint-plugin-react-hooks": "^4.6.0",
|
| 27 |
+
"eslint-plugin-react-refresh": "^0.4.3",
|
| 28 |
+
"terser": "^5.43.1",
|
| 29 |
+
"typescript": "^5.0.2",
|
| 30 |
+
"vite": "^4.4.5"
|
| 31 |
+
}
|
| 32 |
+
}
|
public/songs/AntlerKing2.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c5f71cf80d76266758c8e9daa1c645f3a8d4e616ee969ed5068e7da9227802b2
|
| 3 |
+
size 9168256
|
public/songs/Are_You_One_Of_Us.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:dfb3215d515f7683b5760aee21e8c31471e559f6bbb53e582525416354d20e83
|
| 3 |
+
size 7706176
|
public/songs/Barren_Sky.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ce3cc2374d939bd9e58fb7660f10d90b83fa6aed04b8b778b15bf2a2648f4842
|
| 3 |
+
size 10041856
|
public/songs/Burn_For_You.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4a9f0faa4baf19ecb82a950d06d03bc3726ccb003e86dbc422fe386e957df928
|
| 3 |
+
size 6928576
|
public/songs/Home.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ece72252597b084d127c44ea324297b10df3bdb093a09f63ca2a28f63ee6943f
|
| 3 |
+
size 6928576
|
public/songs/Maybe.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:bf8025a1531bbbe830073696b88f6dc733dc0374d9e44ca9e369dc44e0056ed4
|
| 3 |
+
size 7638976
|
public/songs/Mountain.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d98b1d70d7f9f1635144775c7f7e9a8cf9f159c8a144857e79e2176d94272822
|
| 3 |
+
size 5945536
|
public/songs/Senua.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a4920909303eacce7801a9843130f08703f4540ee81f7ed260c348e3642bdb5f
|
| 3 |
+
size 11046016
|
public/songs/Unraveled.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:eb7ca01646797b4be39248bf9a4e1a56258edfa9cc167c759873db593239fc21
|
| 3 |
+
size 7420096
|
src/.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
src/components/ConfigPanel.tsx
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useEffect } from 'react'
|
| 2 |
+
import configLoader from '../utils/configLoader'
|
| 3 |
+
|
| 4 |
+
interface ConfigPanelProps {
|
| 5 |
+
visible: boolean
|
| 6 |
+
onToggle: () => void
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
export const ConfigPanel: React.FC<ConfigPanelProps> = ({ visible, onToggle }) => {
|
| 10 |
+
const [config, setConfig] = useState<any>(null)
|
| 11 |
+
const [activeSection, setActiveSection] = useState<string>('particles')
|
| 12 |
+
const [searchTerm, setSearchTerm] = useState<string>('')
|
| 13 |
+
|
| 14 |
+
useEffect(() => {
|
| 15 |
+
const loadConfig = async () => {
|
| 16 |
+
const loadedConfig = await configLoader.loadConfig()
|
| 17 |
+
setConfig(loadedConfig)
|
| 18 |
+
}
|
| 19 |
+
loadConfig()
|
| 20 |
+
}, [])
|
| 21 |
+
|
| 22 |
+
const updateConfig = (path: string, value: any) => {
|
| 23 |
+
if (!config) return
|
| 24 |
+
|
| 25 |
+
const pathArray = path.split('.')
|
| 26 |
+
const newConfig = { ...config }
|
| 27 |
+
let current = newConfig
|
| 28 |
+
|
| 29 |
+
// Navigate to the parent object
|
| 30 |
+
for (let i = 0; i < pathArray.length - 1; i++) {
|
| 31 |
+
current = current[pathArray[i]]
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
// Set the value
|
| 35 |
+
current[pathArray[pathArray.length - 1]] = value
|
| 36 |
+
setConfig(newConfig)
|
| 37 |
+
|
| 38 |
+
// Apply to configLoader (this would trigger re-render of visualizer)
|
| 39 |
+
configLoader['config'] = newConfig
|
| 40 |
+
console.log(`🔧 Config updated: ${path} = ${value}`)
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
const applyPreset = (presetName: string) => {
|
| 44 |
+
configLoader.applyPreset(presetName)
|
| 45 |
+
const newConfig = configLoader.getConfig()
|
| 46 |
+
setConfig(newConfig)
|
| 47 |
+
console.log(`🎨 Applied preset: ${presetName}`)
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
const exportConfig = () => {
|
| 51 |
+
if (!config) return
|
| 52 |
+
const configBlob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' })
|
| 53 |
+
const url = URL.createObjectURL(configBlob)
|
| 54 |
+
const a = document.createElement('a')
|
| 55 |
+
a.href = url
|
| 56 |
+
a.download = 'visualizer-config.json'
|
| 57 |
+
a.click()
|
| 58 |
+
URL.revokeObjectURL(url)
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
const reloadConfig = async () => {
|
| 62 |
+
const newConfig = await configLoader.reloadConfig()
|
| 63 |
+
setConfig(newConfig)
|
| 64 |
+
console.log('🔄 Config reloaded from file')
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
const renderConfigSection = (sectionData: any, basePath: string = '') => {
|
| 68 |
+
if (!sectionData) return null
|
| 69 |
+
|
| 70 |
+
return Object.keys(sectionData).map(key => {
|
| 71 |
+
const value = sectionData[key]
|
| 72 |
+
const fullPath = basePath ? `${basePath}.${key}` : key
|
| 73 |
+
|
| 74 |
+
// Filter by search term
|
| 75 |
+
if (searchTerm && !fullPath.toLowerCase().includes(searchTerm.toLowerCase()) &&
|
| 76 |
+
typeof value !== 'object') {
|
| 77 |
+
return null
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
if (typeof value === 'object' && !Array.isArray(value)) {
|
| 81 |
+
return (
|
| 82 |
+
<details key={fullPath} open={!basePath}>
|
| 83 |
+
<summary style={{
|
| 84 |
+
fontWeight: 'bold',
|
| 85 |
+
marginBottom: '8px',
|
| 86 |
+
cursor: 'pointer',
|
| 87 |
+
color: '#FFD700'
|
| 88 |
+
}}>
|
| 89 |
+
{key}
|
| 90 |
+
</summary>
|
| 91 |
+
<div style={{ marginLeft: '16px', borderLeft: '1px solid rgba(255, 215, 0, 0.3)', paddingLeft: '12px' }}>
|
| 92 |
+
{renderConfigSection(value, fullPath)}
|
| 93 |
+
</div>
|
| 94 |
+
</details>
|
| 95 |
+
)
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
return (
|
| 99 |
+
<div key={fullPath} style={{ marginBottom: '8px' }}>
|
| 100 |
+
<label style={{
|
| 101 |
+
display: 'block',
|
| 102 |
+
marginBottom: '4px',
|
| 103 |
+
fontSize: '11px',
|
| 104 |
+
color: '#C9B037'
|
| 105 |
+
}}>
|
| 106 |
+
{key}: <span style={{ opacity: 0.7 }}>({typeof value})</span>
|
| 107 |
+
</label>
|
| 108 |
+
{typeof value === 'boolean' ? (
|
| 109 |
+
<input
|
| 110 |
+
type="checkbox"
|
| 111 |
+
checked={value}
|
| 112 |
+
onChange={(e) => updateConfig(fullPath, e.target.checked)}
|
| 113 |
+
style={{ marginBottom: '4px' }}
|
| 114 |
+
/>
|
| 115 |
+
) : typeof value === 'number' ? (
|
| 116 |
+
<input
|
| 117 |
+
type="number"
|
| 118 |
+
value={value}
|
| 119 |
+
onChange={(e) => updateConfig(fullPath, parseFloat(e.target.value) || 0)}
|
| 120 |
+
step={value < 1 ? 0.001 : value < 10 ? 0.1 : 1}
|
| 121 |
+
style={{
|
| 122 |
+
width: '100%',
|
| 123 |
+
padding: '4px',
|
| 124 |
+
background: 'rgba(0, 0, 0, 0.5)',
|
| 125 |
+
color: '#FFD700',
|
| 126 |
+
border: '1px solid rgba(255, 215, 0, 0.3)',
|
| 127 |
+
borderRadius: '3px',
|
| 128 |
+
fontSize: '10px'
|
| 129 |
+
}}
|
| 130 |
+
/>
|
| 131 |
+
) : typeof value === 'string' ? (
|
| 132 |
+
value.startsWith('#') ? (
|
| 133 |
+
<input
|
| 134 |
+
type="color"
|
| 135 |
+
value={value}
|
| 136 |
+
onChange={(e) => updateConfig(fullPath, e.target.value)}
|
| 137 |
+
style={{ width: '100%', height: '24px' }}
|
| 138 |
+
/>
|
| 139 |
+
) : (
|
| 140 |
+
<input
|
| 141 |
+
type="text"
|
| 142 |
+
value={value}
|
| 143 |
+
onChange={(e) => updateConfig(fullPath, e.target.value)}
|
| 144 |
+
style={{
|
| 145 |
+
width: '100%',
|
| 146 |
+
padding: '4px',
|
| 147 |
+
background: 'rgba(0, 0, 0, 0.5)',
|
| 148 |
+
color: '#FFD700',
|
| 149 |
+
border: '1px solid rgba(255, 215, 0, 0.3)',
|
| 150 |
+
borderRadius: '3px',
|
| 151 |
+
fontSize: '10px'
|
| 152 |
+
}}
|
| 153 |
+
/>
|
| 154 |
+
)
|
| 155 |
+
) : Array.isArray(value) ? (
|
| 156 |
+
<textarea
|
| 157 |
+
value={JSON.stringify(value, null, 2)}
|
| 158 |
+
onChange={(e) => {
|
| 159 |
+
try {
|
| 160 |
+
updateConfig(fullPath, JSON.parse(e.target.value))
|
| 161 |
+
} catch (err) {
|
| 162 |
+
console.warn('Invalid JSON in array field:', err)
|
| 163 |
+
}
|
| 164 |
+
}}
|
| 165 |
+
rows={3}
|
| 166 |
+
style={{
|
| 167 |
+
width: '100%',
|
| 168 |
+
padding: '4px',
|
| 169 |
+
background: 'rgba(0, 0, 0, 0.5)',
|
| 170 |
+
color: '#FFD700',
|
| 171 |
+
border: '1px solid rgba(255, 215, 0, 0.3)',
|
| 172 |
+
borderRadius: '3px',
|
| 173 |
+
fontSize: '9px',
|
| 174 |
+
fontFamily: 'monospace'
|
| 175 |
+
}}
|
| 176 |
+
/>
|
| 177 |
+
) : (
|
| 178 |
+
<span style={{ fontSize: '10px', opacity: 0.7 }}>
|
| 179 |
+
Complex object - edit in JSON
|
| 180 |
+
</span>
|
| 181 |
+
)}
|
| 182 |
+
</div>
|
| 183 |
+
)
|
| 184 |
+
}).filter(Boolean)
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
if (!visible || !config) return null
|
| 188 |
+
|
| 189 |
+
const sections = [
|
| 190 |
+
{ key: 'particles', label: 'Particles', data: config.visualization?.particles },
|
| 191 |
+
{ key: 'spheres', label: 'Spheres', data: config.visualization?.spheres },
|
| 192 |
+
{ key: 'physics', label: 'Physics', data: config.visualization?.physics },
|
| 193 |
+
{ key: 'rotation', label: 'Rotation', data: config.visualization?.rotation },
|
| 194 |
+
{ key: 'audio', label: 'Audio', data: config.audio },
|
| 195 |
+
{ key: 'entronaut', label: 'Entronaut SEFA', data: config.entronaut },
|
| 196 |
+
{ key: 'tendrils', label: 'Tendrils', data: config.tendrils },
|
| 197 |
+
{ key: 'cymatics', label: 'Cymatics', data: config.cymatics },
|
| 198 |
+
{ key: 'colors', label: 'Colors', data: config.colors },
|
| 199 |
+
{ key: 'camera', label: 'Camera', data: config.camera },
|
| 200 |
+
{ key: 'fog', label: 'Fog', data: config.fog },
|
| 201 |
+
{ key: 'rendering', label: 'Rendering', data: config.rendering },
|
| 202 |
+
{ key: 'performance', label: 'Performance', data: config.performance }
|
| 203 |
+
]
|
| 204 |
+
|
| 205 |
+
return (
|
| 206 |
+
<>
|
| 207 |
+
{/* Toggle Button */}
|
| 208 |
+
<button
|
| 209 |
+
onClick={onToggle}
|
| 210 |
+
style={{
|
| 211 |
+
position: 'fixed',
|
| 212 |
+
top: '20px',
|
| 213 |
+
left: '20px',
|
| 214 |
+
zIndex: 10001,
|
| 215 |
+
background: 'rgba(0, 0, 0, 0.8)',
|
| 216 |
+
border: '1px solid rgba(255, 215, 0, 0.5)',
|
| 217 |
+
borderRadius: '4px',
|
| 218 |
+
color: '#FFD700',
|
| 219 |
+
padding: '8px 12px',
|
| 220 |
+
cursor: 'pointer',
|
| 221 |
+
fontSize: '12px',
|
| 222 |
+
fontFamily: 'Cinzel, serif'
|
| 223 |
+
}}
|
| 224 |
+
title="Toggle Config Panel"
|
| 225 |
+
>
|
| 226 |
+
⚙️ Config
|
| 227 |
+
</button>
|
| 228 |
+
|
| 229 |
+
{/* Config Panel */}
|
| 230 |
+
<div
|
| 231 |
+
style={{
|
| 232 |
+
position: 'fixed',
|
| 233 |
+
top: '0',
|
| 234 |
+
left: '0',
|
| 235 |
+
width: '400px',
|
| 236 |
+
height: '100vh',
|
| 237 |
+
background: 'rgba(0, 0, 0, 0.95)',
|
| 238 |
+
color: '#FFD700',
|
| 239 |
+
fontFamily: 'Cinzel, serif',
|
| 240 |
+
zIndex: 10000,
|
| 241 |
+
overflowY: 'auto',
|
| 242 |
+
borderRight: '2px solid rgba(255, 215, 0, 0.3)',
|
| 243 |
+
backdropFilter: 'blur(10px)',
|
| 244 |
+
pointerEvents: 'auto'
|
| 245 |
+
}}
|
| 246 |
+
onMouseEnter={() => {
|
| 247 |
+
// Disable camera controls when mouse enters config panel
|
| 248 |
+
const event = new CustomEvent('disableCameraControls', { detail: true })
|
| 249 |
+
window.dispatchEvent(event)
|
| 250 |
+
}}
|
| 251 |
+
onMouseLeave={() => {
|
| 252 |
+
// Re-enable camera controls when mouse leaves config panel
|
| 253 |
+
const event = new CustomEvent('disableCameraControls', { detail: false })
|
| 254 |
+
window.dispatchEvent(event)
|
| 255 |
+
}}
|
| 256 |
+
>
|
| 257 |
+
<div style={{ padding: '20px' }}>
|
| 258 |
+
{/* Header */}
|
| 259 |
+
<div style={{ marginBottom: '20px' }}>
|
| 260 |
+
<h2 style={{ margin: '0 0 10px 0', fontSize: '18px' }}>
|
| 261 |
+
🛠️ Developer Config
|
| 262 |
+
</h2>
|
| 263 |
+
<div style={{ fontSize: '10px', opacity: 0.8, marginBottom: '15px' }}>
|
| 264 |
+
Live configuration editor
|
| 265 |
+
</div>
|
| 266 |
+
|
| 267 |
+
{/* Search */}
|
| 268 |
+
<input
|
| 269 |
+
type="text"
|
| 270 |
+
placeholder="Search parameters..."
|
| 271 |
+
value={searchTerm}
|
| 272 |
+
onChange={(e) => setSearchTerm(e.target.value)}
|
| 273 |
+
style={{
|
| 274 |
+
width: '100%',
|
| 275 |
+
padding: '6px',
|
| 276 |
+
background: 'rgba(0, 0, 0, 0.5)',
|
| 277 |
+
color: '#FFD700',
|
| 278 |
+
border: '1px solid rgba(255, 215, 0, 0.3)',
|
| 279 |
+
borderRadius: '4px',
|
| 280 |
+
fontSize: '11px',
|
| 281 |
+
marginBottom: '15px'
|
| 282 |
+
}}
|
| 283 |
+
/>
|
| 284 |
+
|
| 285 |
+
{/* Action Buttons */}
|
| 286 |
+
<div style={{
|
| 287 |
+
display: 'grid',
|
| 288 |
+
gridTemplateColumns: '1fr 1fr',
|
| 289 |
+
gap: '8px',
|
| 290 |
+
marginBottom: '15px'
|
| 291 |
+
}}>
|
| 292 |
+
<button
|
| 293 |
+
onClick={exportConfig}
|
| 294 |
+
style={{
|
| 295 |
+
background: 'rgba(255, 215, 0, 0.2)',
|
| 296 |
+
border: '1px solid rgba(255, 215, 0, 0.4)',
|
| 297 |
+
color: '#FFD700',
|
| 298 |
+
padding: '6px',
|
| 299 |
+
borderRadius: '4px',
|
| 300 |
+
fontSize: '10px',
|
| 301 |
+
cursor: 'pointer'
|
| 302 |
+
}}
|
| 303 |
+
>
|
| 304 |
+
💾 Export
|
| 305 |
+
</button>
|
| 306 |
+
<button
|
| 307 |
+
onClick={reloadConfig}
|
| 308 |
+
style={{
|
| 309 |
+
background: 'rgba(255, 215, 0, 0.2)',
|
| 310 |
+
border: '1px solid rgba(255, 215, 0, 0.4)',
|
| 311 |
+
color: '#FFD700',
|
| 312 |
+
padding: '6px',
|
| 313 |
+
borderRadius: '4px',
|
| 314 |
+
fontSize: '10px',
|
| 315 |
+
cursor: 'pointer'
|
| 316 |
+
}}
|
| 317 |
+
>
|
| 318 |
+
🔄 Reload
|
| 319 |
+
</button>
|
| 320 |
+
</div>
|
| 321 |
+
|
| 322 |
+
{/* Presets */}
|
| 323 |
+
{config.presets && Object.keys(config.presets).length > 0 && (
|
| 324 |
+
<div style={{ marginBottom: '15px' }}>
|
| 325 |
+
<div style={{ fontSize: '11px', marginBottom: '5px' }}>Presets:</div>
|
| 326 |
+
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '4px' }}>
|
| 327 |
+
{Object.keys(config.presets).map(presetName => (
|
| 328 |
+
<button
|
| 329 |
+
key={presetName}
|
| 330 |
+
onClick={() => applyPreset(presetName)}
|
| 331 |
+
style={{
|
| 332 |
+
background: 'rgba(255, 215, 0, 0.1)',
|
| 333 |
+
border: '1px solid rgba(255, 215, 0, 0.3)',
|
| 334 |
+
color: '#FFD700',
|
| 335 |
+
padding: '4px',
|
| 336 |
+
borderRadius: '3px',
|
| 337 |
+
fontSize: '9px',
|
| 338 |
+
cursor: 'pointer'
|
| 339 |
+
}}
|
| 340 |
+
>
|
| 341 |
+
{presetName}
|
| 342 |
+
</button>
|
| 343 |
+
))}
|
| 344 |
+
</div>
|
| 345 |
+
</div>
|
| 346 |
+
)}
|
| 347 |
+
</div>
|
| 348 |
+
|
| 349 |
+
{/* Section Tabs */}
|
| 350 |
+
<div style={{
|
| 351 |
+
display: 'flex',
|
| 352 |
+
flexWrap: 'wrap',
|
| 353 |
+
gap: '4px',
|
| 354 |
+
marginBottom: '15px',
|
| 355 |
+
borderBottom: '1px solid rgba(255, 215, 0, 0.3)',
|
| 356 |
+
paddingBottom: '10px'
|
| 357 |
+
}}>
|
| 358 |
+
{sections.map(section => (
|
| 359 |
+
<button
|
| 360 |
+
key={section.key}
|
| 361 |
+
onClick={() => setActiveSection(section.key)}
|
| 362 |
+
style={{
|
| 363 |
+
background: activeSection === section.key ? 'rgba(255, 215, 0, 0.3)' : 'rgba(255, 215, 0, 0.1)',
|
| 364 |
+
border: '1px solid rgba(255, 215, 0, 0.4)',
|
| 365 |
+
color: '#FFD700',
|
| 366 |
+
padding: '4px 8px',
|
| 367 |
+
borderRadius: '3px',
|
| 368 |
+
fontSize: '9px',
|
| 369 |
+
cursor: 'pointer'
|
| 370 |
+
}}
|
| 371 |
+
>
|
| 372 |
+
{section.label}
|
| 373 |
+
</button>
|
| 374 |
+
))}
|
| 375 |
+
</div>
|
| 376 |
+
|
| 377 |
+
{/* Active Section Content */}
|
| 378 |
+
<div style={{ fontSize: '11px' }}>
|
| 379 |
+
{(() => {
|
| 380 |
+
const activeData = sections.find(s => s.key === activeSection)?.data
|
| 381 |
+
if (!activeData) return <div>Section not found</div>
|
| 382 |
+
|
| 383 |
+
return (
|
| 384 |
+
<div>
|
| 385 |
+
<h3 style={{
|
| 386 |
+
margin: '0 0 15px 0',
|
| 387 |
+
fontSize: '14px',
|
| 388 |
+
color: '#FFD700',
|
| 389 |
+
borderBottom: '1px solid rgba(255, 215, 0, 0.2)',
|
| 390 |
+
paddingBottom: '5px'
|
| 391 |
+
}}>
|
| 392 |
+
{sections.find(s => s.key === activeSection)?.label}
|
| 393 |
+
</h3>
|
| 394 |
+
{renderConfigSection(activeData, `${activeSection === 'audio' || activeSection === 'entronaut' || activeSection === 'tendrils' || activeSection === 'cymatics' || activeSection === 'colors' || activeSection === 'camera' || activeSection === 'fog' || activeSection === 'rendering' || activeSection === 'performance' ? activeSection : `visualization.${activeSection}`}`)}
|
| 395 |
+
</div>
|
| 396 |
+
)
|
| 397 |
+
})()}
|
| 398 |
+
</div>
|
| 399 |
+
</div>
|
| 400 |
+
</div>
|
| 401 |
+
</>
|
| 402 |
+
)
|
| 403 |
+
}
|
src/components/ControlPlinth.tsx
ADDED
|
@@ -0,0 +1,681 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useRef, useEffect } from 'react'
|
| 2 |
+
import { useNexusStore } from '../store/nexusStore'
|
| 3 |
+
import type { Track } from '../types/audio'
|
| 4 |
+
|
| 5 |
+
interface AudioControls {
|
| 6 |
+
loadTrack: (track: Track) => Promise<void>
|
| 7 |
+
play: () => Promise<void>
|
| 8 |
+
pause: () => void
|
| 9 |
+
seekTo: (time: number) => void
|
| 10 |
+
setVolume: (volume: number) => void
|
| 11 |
+
getAudioElement?: () => HTMLAudioElement | null
|
| 12 |
+
getCurrentTime?: () => number
|
| 13 |
+
testAudioPlayback?: (url: string) => Promise<boolean>
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
interface ControlPlinthProps {
|
| 17 |
+
tracks: Track[]
|
| 18 |
+
audioControls: AudioControls
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
const ControlPlinth: React.FC<ControlPlinthProps> = ({ tracks, audioControls }) => {
|
| 22 |
+
const [isTestingAudio, setIsTestingAudio] = useState(false)
|
| 23 |
+
const [isDockCollapsed, setIsDockCollapsed] = useState(false)
|
| 24 |
+
const timelineRef = useRef<HTMLDivElement>(null)
|
| 25 |
+
|
| 26 |
+
const {
|
| 27 |
+
playback,
|
| 28 |
+
autoPlayEnabled,
|
| 29 |
+
setIsPlaying,
|
| 30 |
+
setCurrentTrack,
|
| 31 |
+
setVolume,
|
| 32 |
+
setCurrentTime,
|
| 33 |
+
setAutoPlayEnabled,
|
| 34 |
+
getNextTrack,
|
| 35 |
+
getPreviousTrack
|
| 36 |
+
} = useNexusStore()
|
| 37 |
+
|
| 38 |
+
// Load initial track
|
| 39 |
+
useEffect(() => {
|
| 40 |
+
const loadInitialTrack = async () => {
|
| 41 |
+
if (tracks.length > 0 && !playback.currentTrack) {
|
| 42 |
+
try {
|
| 43 |
+
console.log('Loading initial track:', tracks[0].title)
|
| 44 |
+
|
| 45 |
+
// Test basic audio playback first
|
| 46 |
+
if (audioControls.testAudioPlayback) {
|
| 47 |
+
setIsTestingAudio(true)
|
| 48 |
+
console.log('🧪 Running audio test...')
|
| 49 |
+
const canPlay = await audioControls.testAudioPlayback(tracks[0].url)
|
| 50 |
+
setIsTestingAudio(false)
|
| 51 |
+
|
| 52 |
+
if (!canPlay) {
|
| 53 |
+
console.error('❌ Basic audio test failed - audio may not work')
|
| 54 |
+
return
|
| 55 |
+
}
|
| 56 |
+
console.log('✅ Basic audio test passed')
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
setCurrentTrack(tracks[0])
|
| 60 |
+
await audioControls.loadTrack(tracks[0])
|
| 61 |
+
console.log('Initial track loaded successfully')
|
| 62 |
+
} catch (error) {
|
| 63 |
+
console.error('Failed to load initial track:', error)
|
| 64 |
+
setIsTestingAudio(false)
|
| 65 |
+
}
|
| 66 |
+
}
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
loadInitialTrack()
|
| 70 |
+
}, [tracks, playback.currentTrack, setCurrentTrack, audioControls])
|
| 71 |
+
|
| 72 |
+
const handlePlayPause = async () => {
|
| 73 |
+
if (!playback.currentTrack) {
|
| 74 |
+
console.log('No track selected, cannot play')
|
| 75 |
+
return
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
if (isTestingAudio) {
|
| 79 |
+
console.log('Audio test in progress, please wait...')
|
| 80 |
+
return
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
try {
|
| 84 |
+
if (playback.isPlaying) {
|
| 85 |
+
audioControls.pause()
|
| 86 |
+
setIsPlaying(false)
|
| 87 |
+
} else {
|
| 88 |
+
// Ensure track is loaded before playing
|
| 89 |
+
console.log('Starting playback for:', playback.currentTrack.title)
|
| 90 |
+
await audioControls.play()
|
| 91 |
+
setIsPlaying(true)
|
| 92 |
+
}
|
| 93 |
+
} catch (error) {
|
| 94 |
+
console.error('Playback control failed:', error)
|
| 95 |
+
setIsPlaying(false)
|
| 96 |
+
}
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
const handleTrackSelect = async (track: Track) => {
|
| 100 |
+
console.log('Selecting track:', track.title)
|
| 101 |
+
|
| 102 |
+
try {
|
| 103 |
+
// Pause current track if playing
|
| 104 |
+
if (playback.isPlaying) {
|
| 105 |
+
audioControls.pause()
|
| 106 |
+
setIsPlaying(false)
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
// Update UI immediately
|
| 110 |
+
setCurrentTrack(track)
|
| 111 |
+
|
| 112 |
+
// Load the new track
|
| 113 |
+
console.log('Loading track:', track.title)
|
| 114 |
+
await audioControls.loadTrack(track)
|
| 115 |
+
console.log('Track loaded, starting playback')
|
| 116 |
+
|
| 117 |
+
// Start playback
|
| 118 |
+
await audioControls.play()
|
| 119 |
+
setIsPlaying(true)
|
| 120 |
+
} catch (error) {
|
| 121 |
+
console.error('Track selection failed:', error)
|
| 122 |
+
setIsPlaying(false)
|
| 123 |
+
}
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
const handlePrevious = async () => {
|
| 127 |
+
const previousTrack = getPreviousTrack()
|
| 128 |
+
if (previousTrack) {
|
| 129 |
+
await handleTrackSelect(previousTrack)
|
| 130 |
+
}
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
const handleNext = async () => {
|
| 134 |
+
const nextTrack = getNextTrack()
|
| 135 |
+
if (nextTrack) {
|
| 136 |
+
await handleTrackSelect(nextTrack)
|
| 137 |
+
}
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
const handleTimelineClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
| 141 |
+
if (!timelineRef.current || !playback.currentTrack) return
|
| 142 |
+
|
| 143 |
+
const rect = timelineRef.current.getBoundingClientRect()
|
| 144 |
+
const clickX = e.clientX - rect.left
|
| 145 |
+
const percentage = clickX / rect.width
|
| 146 |
+
|
| 147 |
+
// Get actual duration from audio controls
|
| 148 |
+
const audioElement = audioControls.getAudioElement?.()
|
| 149 |
+
const duration = audioElement?.duration || 0
|
| 150 |
+
|
| 151 |
+
if (duration > 0) {
|
| 152 |
+
const newTime = percentage * duration
|
| 153 |
+
audioControls.seekTo(newTime)
|
| 154 |
+
setCurrentTime(newTime)
|
| 155 |
+
}
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
const handleVolumeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
| 159 |
+
const volume = parseFloat(e.target.value)
|
| 160 |
+
setVolume(volume)
|
| 161 |
+
audioControls.setVolume(volume)
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
const formatTime = (seconds: number): string => {
|
| 165 |
+
const mins = Math.floor(seconds / 60)
|
| 166 |
+
const secs = Math.floor(seconds % 60)
|
| 167 |
+
return `${mins}:${secs.toString().padStart(2, '0')}`
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
const getPlayPauseGlyph = () => {
|
| 171 |
+
return playback.isPlaying ? '‖' : '���'
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
const getCurrentProgress = (): number => {
|
| 175 |
+
// Get actual duration and current time
|
| 176 |
+
const audioElement = audioControls.getAudioElement?.()
|
| 177 |
+
const duration = audioElement?.duration || 0
|
| 178 |
+
const currentTime = audioControls.getCurrentTime?.() || playback.currentTime
|
| 179 |
+
|
| 180 |
+
return duration > 0 ? (currentTime / duration) * 100 : 0
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
const getCurrentDuration = (): number => {
|
| 184 |
+
const audioElement = audioControls.getAudioElement?.()
|
| 185 |
+
return audioElement?.duration || 0
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
return (
|
| 189 |
+
<div
|
| 190 |
+
className="control-plinth-dock"
|
| 191 |
+
style={{
|
| 192 |
+
position: 'fixed',
|
| 193 |
+
top: 0,
|
| 194 |
+
left: 0,
|
| 195 |
+
bottom: 0,
|
| 196 |
+
width: isDockCollapsed ? '50px' : '380px',
|
| 197 |
+
background: 'rgba(0, 0, 0, 0.95)',
|
| 198 |
+
border: '1px solid rgba(255, 215, 0, 0.3)',
|
| 199 |
+
borderLeft: 'none',
|
| 200 |
+
borderTopRightRadius: '8px',
|
| 201 |
+
borderBottomRightRadius: '8px',
|
| 202 |
+
backdropFilter: 'blur(15px)',
|
| 203 |
+
transition: 'all 0.3s ease-in-out',
|
| 204 |
+
zIndex: 1000,
|
| 205 |
+
display: 'flex',
|
| 206 |
+
flexDirection: 'column',
|
| 207 |
+
overflow: 'visible',
|
| 208 |
+
pointerEvents: 'auto'
|
| 209 |
+
}}
|
| 210 |
+
onMouseEnter={() => {
|
| 211 |
+
// Disable camera controls when mouse enters audio dock
|
| 212 |
+
const event = new CustomEvent('disableCameraControls', { detail: true })
|
| 213 |
+
window.dispatchEvent(event)
|
| 214 |
+
}}
|
| 215 |
+
onMouseLeave={() => {
|
| 216 |
+
// Re-enable camera controls when mouse leaves audio dock
|
| 217 |
+
const event = new CustomEvent('disableCameraControls', { detail: false })
|
| 218 |
+
window.dispatchEvent(event)
|
| 219 |
+
}}
|
| 220 |
+
>
|
| 221 |
+
{/* Dock Toggle Tab - Prominent and Always Visible */}
|
| 222 |
+
<div
|
| 223 |
+
style={{
|
| 224 |
+
position: 'absolute',
|
| 225 |
+
right: '-45px',
|
| 226 |
+
top: '50%',
|
| 227 |
+
transform: 'translateY(-50%)',
|
| 228 |
+
zIndex: 1003
|
| 229 |
+
}}
|
| 230 |
+
>
|
| 231 |
+
<button
|
| 232 |
+
onClick={() => setIsDockCollapsed(!isDockCollapsed)}
|
| 233 |
+
onMouseEnter={(e) => {
|
| 234 |
+
e.currentTarget.style.transform = 'scale(1.1)'
|
| 235 |
+
e.currentTarget.style.backgroundColor = 'rgba(255, 215, 0, 0.4)'
|
| 236 |
+
e.currentTarget.style.boxShadow = '0 0 20px rgba(255, 215, 0, 0.8)'
|
| 237 |
+
}}
|
| 238 |
+
onMouseLeave={(e) => {
|
| 239 |
+
e.currentTarget.style.transform = 'scale(1)'
|
| 240 |
+
e.currentTarget.style.backgroundColor = 'rgba(0, 0, 0, 0.95)'
|
| 241 |
+
e.currentTarget.style.boxShadow = '2px 0 15px rgba(0, 0, 0, 0.7)'
|
| 242 |
+
}}
|
| 243 |
+
style={{
|
| 244 |
+
background: 'rgba(0, 0, 0, 0.95)',
|
| 245 |
+
border: '3px solid rgba(255, 215, 0, 0.6)',
|
| 246 |
+
borderRadius: '0 15px 15px 0',
|
| 247 |
+
borderLeft: 'none',
|
| 248 |
+
width: '45px',
|
| 249 |
+
height: '90px',
|
| 250 |
+
color: '#FFD700',
|
| 251 |
+
cursor: 'pointer',
|
| 252 |
+
fontSize: '22px',
|
| 253 |
+
display: 'flex',
|
| 254 |
+
alignItems: 'center',
|
| 255 |
+
justifyContent: 'center',
|
| 256 |
+
backdropFilter: 'blur(15px)',
|
| 257 |
+
transition: 'all 0.3s ease-in-out',
|
| 258 |
+
boxShadow: '2px 0 15px rgba(0, 0, 0, 0.7)',
|
| 259 |
+
fontWeight: 'bold',
|
| 260 |
+
outline: 'none'
|
| 261 |
+
}}
|
| 262 |
+
title={isDockCollapsed ? 'Expand Audio Player' : 'Collapse Audio Player'}
|
| 263 |
+
>
|
| 264 |
+
<div style={{
|
| 265 |
+
display: 'flex',
|
| 266 |
+
flexDirection: 'column',
|
| 267 |
+
alignItems: 'center',
|
| 268 |
+
gap: '2px'
|
| 269 |
+
}}>
|
| 270 |
+
<div style={{ fontSize: '18px' }}>{isDockCollapsed ? '▶' : '◀'}</div>
|
| 271 |
+
<div style={{
|
| 272 |
+
fontSize: '8px',
|
| 273 |
+
opacity: 0.8,
|
| 274 |
+
writingMode: 'vertical-rl',
|
| 275 |
+
textOrientation: 'mixed',
|
| 276 |
+
letterSpacing: '1px'
|
| 277 |
+
}}>
|
| 278 |
+
{isDockCollapsed ? 'EXPAND' : 'HIDE'}
|
| 279 |
+
</div>
|
| 280 |
+
</div>
|
| 281 |
+
</button>
|
| 282 |
+
</div>
|
| 283 |
+
|
| 284 |
+
{/* Collapsed State - Enhanced with Progress */}
|
| 285 |
+
{isDockCollapsed && (
|
| 286 |
+
<div style={{
|
| 287 |
+
display: 'flex',
|
| 288 |
+
flexDirection: 'column',
|
| 289 |
+
alignItems: 'center',
|
| 290 |
+
padding: '20px 10px',
|
| 291 |
+
gap: '15px',
|
| 292 |
+
height: '100%',
|
| 293 |
+
justifyContent: 'center',
|
| 294 |
+
position: 'relative'
|
| 295 |
+
}}>
|
| 296 |
+
{/* Vertical Progress Indicator */}
|
| 297 |
+
<div style={{
|
| 298 |
+
position: 'absolute',
|
| 299 |
+
left: '5px',
|
| 300 |
+
top: '20px',
|
| 301 |
+
bottom: '20px',
|
| 302 |
+
width: '3px',
|
| 303 |
+
background: 'rgba(255, 215, 0, 0.3)',
|
| 304 |
+
borderRadius: '2px'
|
| 305 |
+
}}>
|
| 306 |
+
<div style={{
|
| 307 |
+
position: 'absolute',
|
| 308 |
+
bottom: 0,
|
| 309 |
+
width: '100%',
|
| 310 |
+
height: `${getCurrentProgress()}%`,
|
| 311 |
+
background: 'linear-gradient(to top, #FFD700, #FF6B35)',
|
| 312 |
+
borderRadius: '2px',
|
| 313 |
+
transition: 'height 0.1s ease'
|
| 314 |
+
}} />
|
| 315 |
+
</div>
|
| 316 |
+
|
| 317 |
+
<button
|
| 318 |
+
className="central-glyph"
|
| 319 |
+
onClick={handlePlayPause}
|
| 320 |
+
disabled={!playback.currentTrack}
|
| 321 |
+
style={{
|
| 322 |
+
width: '40px',
|
| 323 |
+
height: '40px',
|
| 324 |
+
fontSize: '16px',
|
| 325 |
+
animation: playback.isPlaying ? 'glyphPulse 2s ease-in-out infinite' : 'none'
|
| 326 |
+
}}
|
| 327 |
+
>
|
| 328 |
+
{getPlayPauseGlyph()}
|
| 329 |
+
</button>
|
| 330 |
+
|
| 331 |
+
{/* Volume Indicator */}
|
| 332 |
+
<div style={{
|
| 333 |
+
display: 'flex',
|
| 334 |
+
flexDirection: 'column',
|
| 335 |
+
alignItems: 'center',
|
| 336 |
+
gap: '5px'
|
| 337 |
+
}}>
|
| 338 |
+
<div style={{
|
| 339 |
+
fontSize: '8px',
|
| 340 |
+
color: '#8B6914'
|
| 341 |
+
}}>
|
| 342 |
+
🔊
|
| 343 |
+
</div>
|
| 344 |
+
<div style={{
|
| 345 |
+
width: '2px',
|
| 346 |
+
height: '30px',
|
| 347 |
+
background: 'rgba(255, 215, 0, 0.3)',
|
| 348 |
+
borderRadius: '1px',
|
| 349 |
+
position: 'relative'
|
| 350 |
+
}}>
|
| 351 |
+
<div style={{
|
| 352 |
+
position: 'absolute',
|
| 353 |
+
bottom: 0,
|
| 354 |
+
width: '100%',
|
| 355 |
+
height: `${playback.volume * 100}%`,
|
| 356 |
+
background: '#FFD700',
|
| 357 |
+
borderRadius: '1px',
|
| 358 |
+
transition: 'height 0.2s ease'
|
| 359 |
+
}} />
|
| 360 |
+
</div>
|
| 361 |
+
</div>
|
| 362 |
+
|
| 363 |
+
<div style={{
|
| 364 |
+
writingMode: 'vertical-rl',
|
| 365 |
+
textOrientation: 'mixed',
|
| 366 |
+
fontSize: '9px',
|
| 367 |
+
color: '#FFD700',
|
| 368 |
+
textAlign: 'center',
|
| 369 |
+
maxHeight: '150px',
|
| 370 |
+
overflow: 'hidden',
|
| 371 |
+
lineHeight: '1.2'
|
| 372 |
+
}}>
|
| 373 |
+
{playback.currentTrack?.title || 'No Track'}
|
| 374 |
+
</div>
|
| 375 |
+
|
| 376 |
+
{/* Track Position Indicator with Auto-play Status */}
|
| 377 |
+
{playback.currentTrack && (
|
| 378 |
+
<div style={{
|
| 379 |
+
fontSize: '8px',
|
| 380 |
+
color: '#8B6914',
|
| 381 |
+
textAlign: 'center',
|
| 382 |
+
transform: 'rotate(-90deg)',
|
| 383 |
+
whiteSpace: 'nowrap',
|
| 384 |
+
display: 'flex',
|
| 385 |
+
flexDirection: 'column',
|
| 386 |
+
alignItems: 'center',
|
| 387 |
+
gap: '4px'
|
| 388 |
+
}}>
|
| 389 |
+
<div>{tracks.findIndex(t => t.id === playback.currentTrack?.id) + 1}/{tracks.length}</div>
|
| 390 |
+
{autoPlayEnabled && (
|
| 391 |
+
<div style={{
|
| 392 |
+
fontSize: '6px',
|
| 393 |
+
color: '#FFD700',
|
| 394 |
+
opacity: 0.8
|
| 395 |
+
}}>
|
| 396 |
+
AUTO
|
| 397 |
+
</div>
|
| 398 |
+
)}
|
| 399 |
+
</div>
|
| 400 |
+
)}
|
| 401 |
+
</div>
|
| 402 |
+
)}
|
| 403 |
+
|
| 404 |
+
{/* Expanded State - Full Controls */}
|
| 405 |
+
{!isDockCollapsed && (
|
| 406 |
+
<div style={{
|
| 407 |
+
display: 'flex',
|
| 408 |
+
flexDirection: 'column',
|
| 409 |
+
height: '100%',
|
| 410 |
+
padding: '20px',
|
| 411 |
+
gap: '15px'
|
| 412 |
+
}}>
|
| 413 |
+
{/* Track Information Display */}
|
| 414 |
+
<div className="track-info" style={{
|
| 415 |
+
textAlign: 'center',
|
| 416 |
+
borderBottom: '1px solid rgba(255, 215, 0, 0.2)',
|
| 417 |
+
paddingBottom: '15px'
|
| 418 |
+
}}>
|
| 419 |
+
<div className="track-title" style={{
|
| 420 |
+
fontSize: '16px',
|
| 421 |
+
fontWeight: 600,
|
| 422 |
+
color: '#FFD700',
|
| 423 |
+
marginBottom: '4px'
|
| 424 |
+
}}>
|
| 425 |
+
{playback.currentTrack?.title || 'Select Track'}
|
| 426 |
+
</div>
|
| 427 |
+
<div className="track-artist" style={{
|
| 428 |
+
fontSize: '12px',
|
| 429 |
+
color: '#8B6914'
|
| 430 |
+
}}>
|
| 431 |
+
{playback.currentTrack?.artist || ''}
|
| 432 |
+
</div>
|
| 433 |
+
</div>
|
| 434 |
+
|
| 435 |
+
{/* Main Controls */}
|
| 436 |
+
<div style={{
|
| 437 |
+
display: 'flex',
|
| 438 |
+
flexDirection: 'column',
|
| 439 |
+
alignItems: 'center',
|
| 440 |
+
gap: '15px'
|
| 441 |
+
}}>
|
| 442 |
+
{/* Transport Controls */}
|
| 443 |
+
<div style={{
|
| 444 |
+
display: 'flex',
|
| 445 |
+
alignItems: 'center',
|
| 446 |
+
gap: '10px'
|
| 447 |
+
}}>
|
| 448 |
+
<button
|
| 449 |
+
className="plinth-control"
|
| 450 |
+
onClick={handlePrevious}
|
| 451 |
+
disabled={!playback.currentTrack}
|
| 452 |
+
style={{
|
| 453 |
+
padding: '8px',
|
| 454 |
+
fontSize: '14px'
|
| 455 |
+
}}
|
| 456 |
+
>
|
| 457 |
+
◁
|
| 458 |
+
</button>
|
| 459 |
+
|
| 460 |
+
<button
|
| 461 |
+
className="central-glyph"
|
| 462 |
+
onClick={handlePlayPause}
|
| 463 |
+
disabled={!playback.currentTrack}
|
| 464 |
+
style={{
|
| 465 |
+
width: '50px',
|
| 466 |
+
height: '50px',
|
| 467 |
+
fontSize: '20px'
|
| 468 |
+
}}
|
| 469 |
+
>
|
| 470 |
+
{getPlayPauseGlyph()}
|
| 471 |
+
</button>
|
| 472 |
+
|
| 473 |
+
<button
|
| 474 |
+
className="plinth-control"
|
| 475 |
+
onClick={handleNext}
|
| 476 |
+
disabled={!playback.currentTrack}
|
| 477 |
+
style={{
|
| 478 |
+
padding: '8px',
|
| 479 |
+
fontSize: '14px'
|
| 480 |
+
}}
|
| 481 |
+
>
|
| 482 |
+
▷
|
| 483 |
+
</button>
|
| 484 |
+
</div>
|
| 485 |
+
|
| 486 |
+
{/* Timeline Scrubber */}
|
| 487 |
+
<div style={{ width: '100%' }}>
|
| 488 |
+
<div
|
| 489 |
+
ref={timelineRef}
|
| 490 |
+
className="timeline-channel"
|
| 491 |
+
onClick={handleTimelineClick}
|
| 492 |
+
style={{
|
| 493 |
+
width: '100%',
|
| 494 |
+
height: '8px',
|
| 495 |
+
margin: '0'
|
| 496 |
+
}}
|
| 497 |
+
>
|
| 498 |
+
<div
|
| 499 |
+
className="timeline-indicator"
|
| 500 |
+
style={{ width: `${getCurrentProgress()}%` }}
|
| 501 |
+
/>
|
| 502 |
+
</div>
|
| 503 |
+
|
| 504 |
+
{/* Time Display */}
|
| 505 |
+
<div className="time-display" style={{
|
| 506 |
+
display: 'flex',
|
| 507 |
+
justifyContent: 'space-between',
|
| 508 |
+
gap: '8px',
|
| 509 |
+
fontSize: '10px',
|
| 510 |
+
color: '#8B6914',
|
| 511 |
+
marginTop: '5px'
|
| 512 |
+
}}>
|
| 513 |
+
<span>{formatTime(audioControls.getCurrentTime?.() || playback.currentTime)}</span>
|
| 514 |
+
<span>{formatTime(getCurrentDuration())}</span>
|
| 515 |
+
</div>
|
| 516 |
+
</div>
|
| 517 |
+
|
| 518 |
+
{/* Volume Control */}
|
| 519 |
+
<div className="volume-control" style={{
|
| 520 |
+
display: 'flex',
|
| 521 |
+
flexDirection: 'column',
|
| 522 |
+
alignItems: 'center',
|
| 523 |
+
gap: '8px',
|
| 524 |
+
width: '100%'
|
| 525 |
+
}}>
|
| 526 |
+
<label style={{ fontSize: '12px', color: '#FFD700' }}>Volume</label>
|
| 527 |
+
<input
|
| 528 |
+
type="range"
|
| 529 |
+
min="0"
|
| 530 |
+
max="1"
|
| 531 |
+
step="0.05"
|
| 532 |
+
value={playback.volume}
|
| 533 |
+
onChange={handleVolumeChange}
|
| 534 |
+
className="volume-slider"
|
| 535 |
+
style={{ width: '100%' }}
|
| 536 |
+
/>
|
| 537 |
+
</div>
|
| 538 |
+
|
| 539 |
+
{/* Auto-play Control */}
|
| 540 |
+
<div className="autoplay-control" style={{
|
| 541 |
+
display: 'flex',
|
| 542 |
+
alignItems: 'center',
|
| 543 |
+
justifyContent: 'center',
|
| 544 |
+
gap: '8px',
|
| 545 |
+
width: '100%',
|
| 546 |
+
padding: '8px',
|
| 547 |
+
background: 'rgba(255, 215, 0, 0.1)',
|
| 548 |
+
border: '1px solid rgba(255, 215, 0, 0.3)',
|
| 549 |
+
borderRadius: '6px'
|
| 550 |
+
}}>
|
| 551 |
+
<label style={{
|
| 552 |
+
fontSize: '12px',
|
| 553 |
+
color: '#FFD700',
|
| 554 |
+
cursor: 'pointer',
|
| 555 |
+
display: 'flex',
|
| 556 |
+
alignItems: 'center',
|
| 557 |
+
gap: '6px'
|
| 558 |
+
}}>
|
| 559 |
+
<input
|
| 560 |
+
type="checkbox"
|
| 561 |
+
checked={autoPlayEnabled}
|
| 562 |
+
onChange={(e) => setAutoPlayEnabled(e.target.checked)}
|
| 563 |
+
style={{
|
| 564 |
+
transform: 'scale(1.2)',
|
| 565 |
+
accentColor: '#FFD700'
|
| 566 |
+
}}
|
| 567 |
+
/>
|
| 568 |
+
Auto-play Album
|
| 569 |
+
</label>
|
| 570 |
+
</div>
|
| 571 |
+
</div>
|
| 572 |
+
|
| 573 |
+
{/* Track List - Extended Vertically */}
|
| 574 |
+
<div className="track-list-tablet" style={{
|
| 575 |
+
flex: 1,
|
| 576 |
+
background: 'rgba(0, 0, 0, 0.5)',
|
| 577 |
+
border: '1px solid rgba(255, 215, 0, 0.2)',
|
| 578 |
+
borderRadius: '6px',
|
| 579 |
+
padding: '15px',
|
| 580 |
+
marginTop: '15px',
|
| 581 |
+
overflow: 'hidden',
|
| 582 |
+
display: 'flex',
|
| 583 |
+
flexDirection: 'column',
|
| 584 |
+
minHeight: '300px', // Ensure minimum height for better track visibility
|
| 585 |
+
maxHeight: 'calc(100vh - 280px)' // Allow it to grow but leave space for controls
|
| 586 |
+
}}>
|
| 587 |
+
<div className="tablet-header" style={{
|
| 588 |
+
textAlign: 'center',
|
| 589 |
+
marginBottom: '10px',
|
| 590 |
+
paddingBottom: '8px',
|
| 591 |
+
borderBottom: '1px solid rgba(255, 215, 0, 0.3)'
|
| 592 |
+
}}>
|
| 593 |
+
<h3 style={{
|
| 594 |
+
margin: 0,
|
| 595 |
+
color: '#FFD700',
|
| 596 |
+
fontSize: '12px'
|
| 597 |
+
}}>Album Tracks</h3>
|
| 598 |
+
</div>
|
| 599 |
+
|
| 600 |
+
<div className="track-list" style={{
|
| 601 |
+
display: 'flex',
|
| 602 |
+
flexDirection: 'column',
|
| 603 |
+
gap: '8px',
|
| 604 |
+
overflowY: 'auto',
|
| 605 |
+
overflowX: 'hidden',
|
| 606 |
+
flex: 1,
|
| 607 |
+
paddingRight: '8px' // Add padding for scrollbar
|
| 608 |
+
}}>
|
| 609 |
+
{tracks.map((track, index) => (
|
| 610 |
+
<div
|
| 611 |
+
key={track.id}
|
| 612 |
+
className={`track-item ${
|
| 613 |
+
playback.currentTrack?.id === track.id ? 'active' : ''
|
| 614 |
+
}`}
|
| 615 |
+
onClick={() => handleTrackSelect(track)}
|
| 616 |
+
style={{
|
| 617 |
+
display: 'flex',
|
| 618 |
+
alignItems: 'center',
|
| 619 |
+
gap: '8px',
|
| 620 |
+
padding: '10px 8px',
|
| 621 |
+
borderRadius: '6px',
|
| 622 |
+
cursor: 'pointer',
|
| 623 |
+
transition: 'all 0.2s ease',
|
| 624 |
+
border: '1px solid transparent',
|
| 625 |
+
background: playback.currentTrack?.id === track.id
|
| 626 |
+
? 'linear-gradient(45deg, rgba(255, 215, 0, 0.2), rgba(139, 105, 20, 0.3))'
|
| 627 |
+
: 'transparent',
|
| 628 |
+
borderColor: playback.currentTrack?.id === track.id ? 'rgba(255, 215, 0, 0.4)' : 'transparent'
|
| 629 |
+
}}
|
| 630 |
+
onMouseEnter={(e) => {
|
| 631 |
+
if (playback.currentTrack?.id !== track.id) {
|
| 632 |
+
e.currentTarget.style.background = 'rgba(255, 215, 0, 0.1)'
|
| 633 |
+
e.currentTarget.style.borderColor = 'rgba(255, 215, 0, 0.2)'
|
| 634 |
+
}
|
| 635 |
+
}}
|
| 636 |
+
onMouseLeave={(e) => {
|
| 637 |
+
if (playback.currentTrack?.id !== track.id) {
|
| 638 |
+
e.currentTarget.style.background = 'transparent'
|
| 639 |
+
e.currentTarget.style.borderColor = 'transparent'
|
| 640 |
+
}
|
| 641 |
+
}}
|
| 642 |
+
>
|
| 643 |
+
<span className="track-number" style={{
|
| 644 |
+
fontSize: '10px',
|
| 645 |
+
color: playback.currentTrack?.id === track.id ? '#FFD700' : '#8B6914',
|
| 646 |
+
minWidth: '20px',
|
| 647 |
+
fontWeight: playback.currentTrack?.id === track.id ? 'bold' : 'normal'
|
| 648 |
+
}}>
|
| 649 |
+
{playback.currentTrack?.id === track.id && playback.isPlaying ? '◈' : (index + 1)}
|
| 650 |
+
</span>
|
| 651 |
+
<div className="track-details" style={{
|
| 652 |
+
flex: 1,
|
| 653 |
+
overflow: 'hidden',
|
| 654 |
+
paddingRight: '5px'
|
| 655 |
+
}}>
|
| 656 |
+
<div className="track-name" style={{
|
| 657 |
+
fontSize: '12px',
|
| 658 |
+
color: playback.currentTrack?.id === track.id ? '#FFD700' : '#F5E6D3',
|
| 659 |
+
marginBottom: '2px',
|
| 660 |
+
whiteSpace: 'nowrap',
|
| 661 |
+
overflow: 'hidden',
|
| 662 |
+
textOverflow: 'ellipsis',
|
| 663 |
+
fontWeight: playback.currentTrack?.id === track.id ? 'bold' : 'normal'
|
| 664 |
+
}}>{track.title}</div>
|
| 665 |
+
<div className="track-artist" style={{
|
| 666 |
+
fontSize: '9px',
|
| 667 |
+
color: '#8B6914',
|
| 668 |
+
opacity: 0.8
|
| 669 |
+
}}>{track.artist}</div>
|
| 670 |
+
</div>
|
| 671 |
+
</div>
|
| 672 |
+
))}
|
| 673 |
+
</div>
|
| 674 |
+
</div>
|
| 675 |
+
</div>
|
| 676 |
+
)}
|
| 677 |
+
</div>
|
| 678 |
+
)
|
| 679 |
+
}
|
| 680 |
+
|
| 681 |
+
export default ControlPlinth
|
src/components/LivingNexusApp.tsx
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useEffect, useRef, useCallback, useMemo } from 'react'
|
| 2 |
+
import { useNexusStore } from '../store/nexusStore'
|
| 3 |
+
import { AudioEngine } from '../engine/AudioEngine'
|
| 4 |
+
import { WorldTreeVisualizer } from './WorldTreeVisualizer.tsx'
|
| 5 |
+
import ControlPlinth from './ControlPlinth.tsx'
|
| 6 |
+
import SocialGlyphs from './SocialGlyphs.tsx'
|
| 7 |
+
import type { Track } from '../types/audio'
|
| 8 |
+
|
| 9 |
+
const LivingNexusApp: React.FC = () => {
|
| 10 |
+
const audioEngineRef = useRef<AudioEngine | null>(null)
|
| 11 |
+
const lastTimeRef = useRef<number>(0)
|
| 12 |
+
|
| 13 |
+
const {
|
| 14 |
+
isAudioInitialized,
|
| 15 |
+
playback,
|
| 16 |
+
interactionState,
|
| 17 |
+
autoPlayEnabled,
|
| 18 |
+
updateAudioData,
|
| 19 |
+
setCurrentTime,
|
| 20 |
+
setPlaybackState,
|
| 21 |
+
setCurrentTrack,
|
| 22 |
+
setIsPlaying,
|
| 23 |
+
initializeAudio,
|
| 24 |
+
getNextTrack
|
| 25 |
+
} = useNexusStore()
|
| 26 |
+
|
| 27 |
+
// The Living Nexus Album Tracks
|
| 28 |
+
const albumTracks: Track[] = useMemo(() => [
|
| 29 |
+
{
|
| 30 |
+
id: '1',
|
| 31 |
+
title: 'AntlerKing2',
|
| 32 |
+
artist: 'The Living Nexus',
|
| 33 |
+
url: '/songs/AntlerKing2.mp3'
|
| 34 |
+
},
|
| 35 |
+
{
|
| 36 |
+
id: '2',
|
| 37 |
+
title: 'Are You One Of Us?',
|
| 38 |
+
artist: 'The Living Nexus',
|
| 39 |
+
url: '/songs/Are_You_One_Of_Us.mp3'
|
| 40 |
+
},
|
| 41 |
+
{
|
| 42 |
+
id: '3',
|
| 43 |
+
title: 'Barren Sky',
|
| 44 |
+
artist: 'The Living Nexus',
|
| 45 |
+
url: '/songs/Barren_Sky.mp3'
|
| 46 |
+
},
|
| 47 |
+
{
|
| 48 |
+
id: '4',
|
| 49 |
+
title: 'Burn For You',
|
| 50 |
+
artist: 'The Living Nexus',
|
| 51 |
+
url: '/songs/Burn_For_You.mp3'
|
| 52 |
+
},
|
| 53 |
+
{
|
| 54 |
+
id: '5',
|
| 55 |
+
title: 'Home',
|
| 56 |
+
artist: 'The Living Nexus',
|
| 57 |
+
url: '/songs/Home.mp3'
|
| 58 |
+
},
|
| 59 |
+
{
|
| 60 |
+
id: '6',
|
| 61 |
+
title: 'Maybe',
|
| 62 |
+
artist: 'The Living Nexus',
|
| 63 |
+
url: '/songs/Maybe.mp3'
|
| 64 |
+
},
|
| 65 |
+
{
|
| 66 |
+
id: '7',
|
| 67 |
+
title: 'Mountain',
|
| 68 |
+
artist: 'The Living Nexus',
|
| 69 |
+
url: '/songs/Mountain.mp3'
|
| 70 |
+
},
|
| 71 |
+
{
|
| 72 |
+
id: '8',
|
| 73 |
+
title: 'Senua',
|
| 74 |
+
artist: 'The Living Nexus',
|
| 75 |
+
url: '/songs/Senua.mp3'
|
| 76 |
+
},
|
| 77 |
+
{
|
| 78 |
+
id: '9',
|
| 79 |
+
title: 'Unraveled',
|
| 80 |
+
artist: 'The Living Nexus',
|
| 81 |
+
url: '/songs/Unraveled.mp3'
|
| 82 |
+
}
|
| 83 |
+
], [])
|
| 84 |
+
|
| 85 |
+
// Audio engine control methods - memoized to prevent re-renders
|
| 86 |
+
const audioControls = useMemo(() => ({
|
| 87 |
+
async loadTrack(track: Track) {
|
| 88 |
+
if (audioEngineRef.current) {
|
| 89 |
+
console.log('AudioControls: Loading track', track.title)
|
| 90 |
+
await audioEngineRef.current.loadTrack(track)
|
| 91 |
+
}
|
| 92 |
+
},
|
| 93 |
+
|
| 94 |
+
async play() {
|
| 95 |
+
if (audioEngineRef.current) {
|
| 96 |
+
console.log('AudioControls: Starting playback')
|
| 97 |
+
await audioEngineRef.current.play()
|
| 98 |
+
}
|
| 99 |
+
},
|
| 100 |
+
|
| 101 |
+
pause() {
|
| 102 |
+
if (audioEngineRef.current) {
|
| 103 |
+
console.log('AudioControls: Pausing playback')
|
| 104 |
+
audioEngineRef.current.pause()
|
| 105 |
+
}
|
| 106 |
+
},
|
| 107 |
+
|
| 108 |
+
seekTo(time: number) {
|
| 109 |
+
if (audioEngineRef.current) {
|
| 110 |
+
audioEngineRef.current.seekTo(time)
|
| 111 |
+
}
|
| 112 |
+
},
|
| 113 |
+
|
| 114 |
+
setVolume(volume: number) {
|
| 115 |
+
if (audioEngineRef.current) {
|
| 116 |
+
audioEngineRef.current.setVolume(volume)
|
| 117 |
+
}
|
| 118 |
+
},
|
| 119 |
+
|
| 120 |
+
getAudioElement() {
|
| 121 |
+
return audioEngineRef.current?.getAudioElement() || null
|
| 122 |
+
},
|
| 123 |
+
|
| 124 |
+
getCurrentTime() {
|
| 125 |
+
return audioEngineRef.current?.getCurrentTime() || 0
|
| 126 |
+
},
|
| 127 |
+
|
| 128 |
+
getDuration() {
|
| 129 |
+
return audioEngineRef.current?.getDuration() || 0
|
| 130 |
+
},
|
| 131 |
+
|
| 132 |
+
isPlaying() {
|
| 133 |
+
return audioEngineRef.current?.isPlaying() || false
|
| 134 |
+
},
|
| 135 |
+
|
| 136 |
+
async testAudioPlayback(url: string) {
|
| 137 |
+
if (audioEngineRef.current && audioEngineRef.current.testAudioPlayback) {
|
| 138 |
+
return await audioEngineRef.current.testAudioPlayback(url)
|
| 139 |
+
}
|
| 140 |
+
return false
|
| 141 |
+
}
|
| 142 |
+
}), [])
|
| 143 |
+
|
| 144 |
+
// Auto-play next track function
|
| 145 |
+
const handleAutoPlayNext = useCallback(async () => {
|
| 146 |
+
if (!autoPlayEnabled) {
|
| 147 |
+
console.log('🎵 Auto-play disabled, stopping playback')
|
| 148 |
+
setIsPlaying(false)
|
| 149 |
+
return
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
const nextTrack = getNextTrack()
|
| 153 |
+
if (nextTrack) {
|
| 154 |
+
console.log('🎵 Auto-playing next track:', nextTrack.title)
|
| 155 |
+
try {
|
| 156 |
+
setCurrentTrack(nextTrack)
|
| 157 |
+
await audioControls.loadTrack(nextTrack)
|
| 158 |
+
await audioControls.play()
|
| 159 |
+
setIsPlaying(true)
|
| 160 |
+
} catch (error) {
|
| 161 |
+
console.error('❌ Auto-play failed:', error)
|
| 162 |
+
setIsPlaying(false)
|
| 163 |
+
}
|
| 164 |
+
} else {
|
| 165 |
+
console.log('🎵 Reached end of album, stopping playback')
|
| 166 |
+
setIsPlaying(false)
|
| 167 |
+
}
|
| 168 |
+
}, [autoPlayEnabled, getNextTrack, audioControls, setCurrentTrack, setIsPlaying])
|
| 169 |
+
|
| 170 |
+
// Initialize tracks in store and set up auto-play
|
| 171 |
+
useEffect(() => {
|
| 172 |
+
// Initialize tracks in store
|
| 173 |
+
setPlaybackState({ tracks: albumTracks })
|
| 174 |
+
console.log('🎵 Initialized', albumTracks.length, 'tracks in store')
|
| 175 |
+
}, [albumTracks, setPlaybackState])
|
| 176 |
+
|
| 177 |
+
// Set up auto-play callback when audio engine is ready
|
| 178 |
+
useEffect(() => {
|
| 179 |
+
if (audioEngineRef.current && isAudioInitialized) {
|
| 180 |
+
audioEngineRef.current.setOnTrackEndedCallback(handleAutoPlayNext)
|
| 181 |
+
console.log('🎵 Auto-play callback registered')
|
| 182 |
+
}
|
| 183 |
+
}, [isAudioInitialized, handleAutoPlayNext])
|
| 184 |
+
|
| 185 |
+
// Initialize audio engine
|
| 186 |
+
useEffect(() => {
|
| 187 |
+
const initializeAudioEngine = async () => {
|
| 188 |
+
console.log('🎵 Initializing AudioEngine...')
|
| 189 |
+
try {
|
| 190 |
+
audioEngineRef.current = new AudioEngine()
|
| 191 |
+
await audioEngineRef.current.initialize()
|
| 192 |
+
initializeAudio()
|
| 193 |
+
console.log('🎵 Living Nexus audio initialized')
|
| 194 |
+
} catch (error) {
|
| 195 |
+
console.error('❌ Failed to initialize audio:', error)
|
| 196 |
+
}
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
// Only initialize once
|
| 200 |
+
if (!isAudioInitialized && !audioEngineRef.current) {
|
| 201 |
+
initializeAudioEngine()
|
| 202 |
+
}
|
| 203 |
+
}, [isAudioInitialized, initializeAudio])
|
| 204 |
+
|
| 205 |
+
// Cleanup only on unmount
|
| 206 |
+
useEffect(() => {
|
| 207 |
+
return () => {
|
| 208 |
+
if (audioEngineRef.current) {
|
| 209 |
+
audioEngineRef.current.cleanup()
|
| 210 |
+
audioEngineRef.current = null
|
| 211 |
+
}
|
| 212 |
+
}
|
| 213 |
+
}, [])
|
| 214 |
+
|
| 215 |
+
// Animation loop for audio analysis and visualization updates
|
| 216 |
+
useEffect(() => {
|
| 217 |
+
let animationId: number
|
| 218 |
+
|
| 219 |
+
const animate = (currentTime: number) => {
|
| 220 |
+
const deltaTime = lastTimeRef.current ? (currentTime - lastTimeRef.current) / 1000 : 0
|
| 221 |
+
lastTimeRef.current = currentTime
|
| 222 |
+
|
| 223 |
+
if (audioEngineRef.current) {
|
| 224 |
+
// Update beat manager if alive
|
| 225 |
+
if (interactionState.isAlive) {
|
| 226 |
+
audioEngineRef.current.updateBeatManager(deltaTime)
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
// Always get audio data for visualization
|
| 230 |
+
const audioData = audioEngineRef.current.getAudioData()
|
| 231 |
+
updateAudioData(audioData)
|
| 232 |
+
|
| 233 |
+
// Update current time from actual audio element
|
| 234 |
+
if (playback.isPlaying && playback.currentTrack && audioEngineRef.current.getCurrentTime) {
|
| 235 |
+
const actualCurrentTime = audioEngineRef.current.getCurrentTime()
|
| 236 |
+
if (Math.abs(actualCurrentTime - playback.currentTime) > 0.5) {
|
| 237 |
+
setCurrentTime(actualCurrentTime)
|
| 238 |
+
}
|
| 239 |
+
}
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
animationId = requestAnimationFrame(animate)
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
// Start animation loop when audio is initialized
|
| 246 |
+
if (isAudioInitialized) {
|
| 247 |
+
animationId = requestAnimationFrame(animate)
|
| 248 |
+
|
| 249 |
+
return () => {
|
| 250 |
+
if (animationId) {
|
| 251 |
+
cancelAnimationFrame(animationId)
|
| 252 |
+
}
|
| 253 |
+
}
|
| 254 |
+
}
|
| 255 |
+
}, [isAudioInitialized, interactionState.isAlive, playback.isPlaying, playback.currentTrack, updateAudioData, setCurrentTime])
|
| 256 |
+
|
| 257 |
+
// Determine the overall artifact state classes
|
| 258 |
+
const getArtifactClasses = useCallback(() => {
|
| 259 |
+
const classes = ['living-nexus-artifact']
|
| 260 |
+
|
| 261 |
+
if (interactionState.isDormant) {
|
| 262 |
+
classes.push('dormant')
|
| 263 |
+
} else if (interactionState.isAwakening) {
|
| 264 |
+
classes.push('awakening')
|
| 265 |
+
} else if (interactionState.isAlive) {
|
| 266 |
+
classes.push('alive')
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
return classes.join(' ')
|
| 270 |
+
}, [interactionState.isDormant, interactionState.isAwakening, interactionState.isAlive])
|
| 271 |
+
|
| 272 |
+
return (
|
| 273 |
+
<div className={getArtifactClasses()}>
|
| 274 |
+
{/* Main Visualization Container - Full Viewport */}
|
| 275 |
+
<div className="visualization-container">
|
| 276 |
+
<WorldTreeVisualizer audioEngine={audioEngineRef.current} />
|
| 277 |
+
</div>
|
| 278 |
+
|
| 279 |
+
{/* Audio Player Interface - Fixed Bottom */}
|
| 280 |
+
<div className="audio-player-container">
|
| 281 |
+
<ControlPlinth
|
| 282 |
+
tracks={albumTracks}
|
| 283 |
+
audioControls={audioControls}
|
| 284 |
+
/>
|
| 285 |
+
</div>
|
| 286 |
+
|
| 287 |
+
{/* Social Media Glyphs - Fixed Top */}
|
| 288 |
+
<SocialGlyphs />
|
| 289 |
+
</div>
|
| 290 |
+
)
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
export default LivingNexusApp
|
src/components/SocialGlyphs.tsx
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState } from 'react'
|
| 2 |
+
import { useNexusStore } from '../store/nexusStore'
|
| 3 |
+
|
| 4 |
+
const SocialGlyphs: React.FC = () => {
|
| 5 |
+
const [hoveredGlyph, setHoveredGlyph] = useState<string | null>(null)
|
| 6 |
+
const { socialLinks } = useNexusStore()
|
| 7 |
+
|
| 8 |
+
const handleGlyphClick = (url: string) => {
|
| 9 |
+
window.open(url, '_blank', 'noopener,noreferrer')
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
const getGlyphData = (platform: string) => {
|
| 13 |
+
switch (platform.toLowerCase()) {
|
| 14 |
+
case 'spotify':
|
| 15 |
+
return { icon: '◉', color: '#1DB954', label: 'Spotify' }
|
| 16 |
+
case 'apple music':
|
| 17 |
+
return { icon: '⚹', color: '#FA243C', label: 'Apple Music' }
|
| 18 |
+
case 'instagram':
|
| 19 |
+
return { icon: '⟡', color: '#E4405F', label: 'Instagram' }
|
| 20 |
+
case 'youtube':
|
| 21 |
+
return { icon: '◈', color: '#FF0000', label: 'YouTube' }
|
| 22 |
+
case 'twitter':
|
| 23 |
+
return { icon: '◊', color: '#1DA1F2', label: 'Twitter' }
|
| 24 |
+
case 'facebook':
|
| 25 |
+
return { icon: '⬢', color: '#1877F2', label: 'Facebook' }
|
| 26 |
+
case 'bandcamp':
|
| 27 |
+
return { icon: '♮', color: '#629AA0', label: 'Bandcamp' }
|
| 28 |
+
case 'soundcloud':
|
| 29 |
+
return { icon: '◊', color: '#FF8800', label: 'SoundCloud' }
|
| 30 |
+
case 'website':
|
| 31 |
+
return { icon: '⟐', color: '#FFD700', label: 'Website' }
|
| 32 |
+
case 'email':
|
| 33 |
+
return { icon: '✦', color: '#D14836', label: 'Email' }
|
| 34 |
+
default:
|
| 35 |
+
return { icon: '⟡', color: '#FFD700', label: platform }
|
| 36 |
+
}
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
return (
|
| 40 |
+
<div style={{
|
| 41 |
+
position: 'fixed',
|
| 42 |
+
top: '20px',
|
| 43 |
+
right: '20px',
|
| 44 |
+
zIndex: 1000,
|
| 45 |
+
display: 'flex',
|
| 46 |
+
gap: '10px',
|
| 47 |
+
padding: '10px 15px',
|
| 48 |
+
background: 'rgba(0, 0, 0, 0.8)',
|
| 49 |
+
borderRadius: '25px',
|
| 50 |
+
backdropFilter: 'blur(10px)',
|
| 51 |
+
border: '1px solid rgba(255, 215, 0, 0.2)',
|
| 52 |
+
fontFamily: 'Cinzel, serif'
|
| 53 |
+
}}>
|
| 54 |
+
{socialLinks.map((link) => {
|
| 55 |
+
const glyphData = getGlyphData(link.platform)
|
| 56 |
+
const isHovered = hoveredGlyph === link.id
|
| 57 |
+
|
| 58 |
+
return (
|
| 59 |
+
<div
|
| 60 |
+
key={link.id}
|
| 61 |
+
onClick={() => handleGlyphClick(link.url)}
|
| 62 |
+
onMouseEnter={() => setHoveredGlyph(link.id)}
|
| 63 |
+
onMouseLeave={() => setHoveredGlyph(null)}
|
| 64 |
+
style={{
|
| 65 |
+
position: 'relative',
|
| 66 |
+
width: '35px',
|
| 67 |
+
height: '35px',
|
| 68 |
+
display: 'flex',
|
| 69 |
+
alignItems: 'center',
|
| 70 |
+
justifyContent: 'center',
|
| 71 |
+
borderRadius: '50%',
|
| 72 |
+
background: isHovered
|
| 73 |
+
? `linear-gradient(135deg, ${glyphData.color}22, ${glyphData.color}44)`
|
| 74 |
+
: 'rgba(255, 215, 0, 0.1)',
|
| 75 |
+
border: `1px solid ${isHovered ? glyphData.color : 'rgba(255, 215, 0, 0.3)'}`,
|
| 76 |
+
cursor: 'pointer',
|
| 77 |
+
transition: 'all 0.3s ease-in-out',
|
| 78 |
+
transform: isHovered ? 'scale(1.1)' : 'scale(1)',
|
| 79 |
+
boxShadow: isHovered
|
| 80 |
+
? `0 4px 15px ${glyphData.color}33`
|
| 81 |
+
: '0 2px 8px rgba(0, 0, 0, 0.3)',
|
| 82 |
+
fontSize: '14px',
|
| 83 |
+
color: isHovered ? glyphData.color : '#FFD700'
|
| 84 |
+
}}
|
| 85 |
+
>
|
| 86 |
+
{glyphData.icon}
|
| 87 |
+
|
| 88 |
+
{/* Enhanced Tooltip */}
|
| 89 |
+
{isHovered && (
|
| 90 |
+
<div style={{
|
| 91 |
+
position: 'absolute',
|
| 92 |
+
top: '60px',
|
| 93 |
+
left: '50%',
|
| 94 |
+
transform: 'translateX(-50%)',
|
| 95 |
+
background: 'rgba(0, 0, 0, 0.95)',
|
| 96 |
+
color: glyphData.color,
|
| 97 |
+
padding: '8px 12px',
|
| 98 |
+
borderRadius: '8px',
|
| 99 |
+
fontSize: '11px',
|
| 100 |
+
fontWeight: 'bold',
|
| 101 |
+
whiteSpace: 'nowrap',
|
| 102 |
+
border: `1px solid ${glyphData.color}`,
|
| 103 |
+
backdropFilter: 'blur(10px)',
|
| 104 |
+
animation: 'fadeInDown 0.2s ease-out',
|
| 105 |
+
boxShadow: `0 4px 15px ${glyphData.color}33`
|
| 106 |
+
}}>
|
| 107 |
+
{glyphData.label}
|
| 108 |
+
{/* Tooltip Arrow */}
|
| 109 |
+
<div style={{
|
| 110 |
+
position: 'absolute',
|
| 111 |
+
bottom: '100%',
|
| 112 |
+
left: '50%',
|
| 113 |
+
transform: 'translateX(-50%)',
|
| 114 |
+
width: 0,
|
| 115 |
+
height: 0,
|
| 116 |
+
borderLeft: '6px solid transparent',
|
| 117 |
+
borderRight: '6px solid transparent',
|
| 118 |
+
borderBottom: `6px solid ${glyphData.color}`
|
| 119 |
+
}} />
|
| 120 |
+
</div>
|
| 121 |
+
)}
|
| 122 |
+
|
| 123 |
+
{/* Pulsing Ring Effect for Music Platforms */}
|
| 124 |
+
{(link.platform.toLowerCase().includes('spotify') ||
|
| 125 |
+
link.platform.toLowerCase().includes('bandcamp') ||
|
| 126 |
+
link.platform.toLowerCase().includes('soundcloud')) && isHovered && (
|
| 127 |
+
<div style={{
|
| 128 |
+
position: 'absolute',
|
| 129 |
+
width: '100%',
|
| 130 |
+
height: '100%',
|
| 131 |
+
borderRadius: '50%',
|
| 132 |
+
border: `2px solid ${glyphData.color}`,
|
| 133 |
+
animation: 'pulse 1.5s infinite',
|
| 134 |
+
pointerEvents: 'none'
|
| 135 |
+
}} />
|
| 136 |
+
)}
|
| 137 |
+
</div>
|
| 138 |
+
)
|
| 139 |
+
})}
|
| 140 |
+
|
| 141 |
+
{/* Add CSS animations via style tag */}
|
| 142 |
+
<style>
|
| 143 |
+
{`
|
| 144 |
+
@keyframes fadeInDown {
|
| 145 |
+
from {
|
| 146 |
+
opacity: 0;
|
| 147 |
+
transform: translateX(-50%) translateY(-10px);
|
| 148 |
+
}
|
| 149 |
+
to {
|
| 150 |
+
opacity: 1;
|
| 151 |
+
transform: translateX(-50%) translateY(0);
|
| 152 |
+
}
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
@keyframes pulse {
|
| 156 |
+
0% {
|
| 157 |
+
transform: scale(1);
|
| 158 |
+
opacity: 1;
|
| 159 |
+
}
|
| 160 |
+
50% {
|
| 161 |
+
transform: scale(1.2);
|
| 162 |
+
opacity: 0.5;
|
| 163 |
+
}
|
| 164 |
+
100% {
|
| 165 |
+
transform: scale(1.4);
|
| 166 |
+
opacity: 0;
|
| 167 |
+
}
|
| 168 |
+
}
|
| 169 |
+
`}
|
| 170 |
+
</style>
|
| 171 |
+
</div>
|
| 172 |
+
)
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
export default SocialGlyphs
|
src/components/WorldTreeVisualizer.tsx
ADDED
|
@@ -0,0 +1,2394 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useRef, useEffect, useState, useCallback } from 'react'
|
| 2 |
+
import { AudioEngine } from '../engine/AudioEngine'
|
| 3 |
+
import {
|
| 4 |
+
ParticleSystemParams,
|
| 5 |
+
ParticleSphere,
|
| 6 |
+
DEFAULT_FREQUENCY_RANGES,
|
| 7 |
+
DEFAULT_SPHERE_PARAMS,
|
| 8 |
+
FogParams
|
| 9 |
+
} from '../types/visualization'
|
| 10 |
+
import { NoiseGenerator, BeatManager, generateNewNoiseScale } from '../utils/noise'
|
| 11 |
+
import configLoader from '../utils/configLoader'
|
| 12 |
+
|
| 13 |
+
interface WorldTreeVisualizerProps {
|
| 14 |
+
audioEngine: AudioEngine | null
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
// ENTRONAUT INTEGRATION: Symbolic Emergence Field Analysis
|
| 18 |
+
interface EntronautState {
|
| 19 |
+
sefaScore: Float32Array
|
| 20 |
+
emergenceField: Float32Array
|
| 21 |
+
adaptiveParams: {
|
| 22 |
+
damping: Float32Array
|
| 23 |
+
diffusion: Float32Array
|
| 24 |
+
coupling: Float32Array
|
| 25 |
+
}
|
| 26 |
+
informationMetrics: {
|
| 27 |
+
complexity: number
|
| 28 |
+
emergence: number
|
| 29 |
+
coherence: number
|
| 30 |
+
}
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
// Advanced 3D Particle System Visualizer - Enhanced with Entronaut SEFA
|
| 34 |
+
export const WorldTreeVisualizer: React.FC<WorldTreeVisualizerProps> = ({ audioEngine }) => {
|
| 35 |
+
const canvasRef = useRef<HTMLCanvasElement>(null)
|
| 36 |
+
const animationRef = useRef<number>()
|
| 37 |
+
const lastTimeRef = useRef<number>(0)
|
| 38 |
+
|
| 39 |
+
// Three.js core objects
|
| 40 |
+
const sceneRef = useRef<any>()
|
| 41 |
+
const rendererRef = useRef<any>()
|
| 42 |
+
const cameraRef = useRef<any>()
|
| 43 |
+
const controlsRef = useRef<any>()
|
| 44 |
+
|
| 45 |
+
// Advanced visualization state
|
| 46 |
+
const spheresRef = useRef<ParticleSphere[]>([])
|
| 47 |
+
const noiseGeneratorRef = useRef<NoiseGenerator>()
|
| 48 |
+
const beatManagerRef = useRef<BeatManager>()
|
| 49 |
+
const tendrilSystemRef = useRef<any>()
|
| 50 |
+
const dysonSphereRef = useRef<any>()
|
| 51 |
+
|
| 52 |
+
// ENTRONAUT: Symbolic emergence tracking
|
| 53 |
+
const entronautStateRef = useRef<EntronautState>()
|
| 54 |
+
|
| 55 |
+
// State for UI controls
|
| 56 |
+
const [isVisualizationReady, setIsVisualizationReady] = useState(false)
|
| 57 |
+
const [activeSpheresCount, setActiveSpheresCount] = useState(5) // All spheres active by default
|
| 58 |
+
const [cymateGeometry, setCymateGeometry] = useState(true)
|
| 59 |
+
const [entronautEnabled, setEntronautEnabled] = useState(true)
|
| 60 |
+
const [adaptiveCoupling, setAdaptiveCoupling] = useState(true)
|
| 61 |
+
const [performanceMode, setPerformanceMode] = useState(false)
|
| 62 |
+
const [controlsCollapsed, setControlsCollapsed] = useState(true)
|
| 63 |
+
const [cameraControlsEnabled, setCameraControlsEnabled] = useState(true)
|
| 64 |
+
const [autoRotate, setAutoRotate] = useState(false)
|
| 65 |
+
const [aboutCollapsed, setAboutCollapsed] = useState(true)
|
| 66 |
+
const [tendrilsEnabled, setTendrilsEnabled] = useState(true)
|
| 67 |
+
const [tendrilDensity, setTendrilDensity] = useState(0.3)
|
| 68 |
+
const [dysonSphereEnabled, setDysonSphereEnabled] = useState(true)
|
| 69 |
+
const [vineGrowthRate, setVineGrowthRate] = useState(0.02)
|
| 70 |
+
const [vineComplexity, setVineComplexity] = useState(24)
|
| 71 |
+
const [fogParams, setFogParams] = useState<FogParams>({
|
| 72 |
+
enabled: true,
|
| 73 |
+
color: '#000000',
|
| 74 |
+
near: 2.7,
|
| 75 |
+
far: 3.7
|
| 76 |
+
})
|
| 77 |
+
|
| 78 |
+
// Store state (currently minimal, expanded as needed)
|
| 79 |
+
// const { playback } = useNexusStore()
|
| 80 |
+
|
| 81 |
+
// Simplified performance monitoring - no complex optimizer
|
| 82 |
+
const [performanceStats, setPerformanceStats] = useState({
|
| 83 |
+
fps: 60,
|
| 84 |
+
particleCount: 0,
|
| 85 |
+
lastFpsUpdate: 0,
|
| 86 |
+
frameCount: 0
|
| 87 |
+
})
|
| 88 |
+
|
| 89 |
+
// ENTRONAUT: Initialize emergence field analysis - SIMPLIFIED for performance
|
| 90 |
+
const initializeEntronaut = useCallback(() => {
|
| 91 |
+
// Simplified - basic structure for UI display only
|
| 92 |
+
console.log('🧠 Entronaut SEFA system simplified for performance')
|
| 93 |
+
entronautStateRef.current = {
|
| 94 |
+
sefaScore: new Float32Array(0),
|
| 95 |
+
emergenceField: new Float32Array(0),
|
| 96 |
+
adaptiveParams: {
|
| 97 |
+
damping: new Float32Array(0),
|
| 98 |
+
diffusion: new Float32Array(0),
|
| 99 |
+
coupling: new Float32Array(0)
|
| 100 |
+
},
|
| 101 |
+
informationMetrics: {
|
| 102 |
+
complexity: 0,
|
| 103 |
+
emergence: 0,
|
| 104 |
+
coherence: 0
|
| 105 |
+
}
|
| 106 |
+
}
|
| 107 |
+
}, [])
|
| 108 |
+
|
| 109 |
+
// ENTRONAUT: Analyze audio - SIMPLIFIED
|
| 110 |
+
const analyzeSymbolicEmergence = useCallback((audioData: any, currentTime: number) => {
|
| 111 |
+
// Simplified - only basic metrics without complex calculations
|
| 112 |
+
if (!entronautStateRef.current || !entronautEnabled) return
|
| 113 |
+
|
| 114 |
+
// Just update basic metrics without heavy computation
|
| 115 |
+
const { informationMetrics } = entronautStateRef.current
|
| 116 |
+
informationMetrics.complexity = audioData.overallAmplitude || 0
|
| 117 |
+
informationMetrics.emergence = audioData.beatDetected ? 1.0 : 0.1
|
| 118 |
+
informationMetrics.coherence = (audioData.deepEnergy + audioData.midEnergy + audioData.highEnergy) / 3.0
|
| 119 |
+
|
| 120 |
+
}, [entronautEnabled])
|
| 121 |
+
|
| 122 |
+
// ENTRONAUT: Apply adaptive coupling - SIMPLIFIED
|
| 123 |
+
const applyEntronautCoupling = useCallback((sphere: ParticleSphere, particleIndex: number) => {
|
| 124 |
+
// Return simple defaults - no complex field calculations
|
| 125 |
+
return { damping: 0.95, diffusion: 0.008, coupling: 0.02 }
|
| 126 |
+
}, [])
|
| 127 |
+
|
| 128 |
+
// Initialize Three.js scene and particle systems
|
| 129 |
+
const initializeVisualization = useCallback(async () => {
|
| 130 |
+
if (!canvasRef.current) return
|
| 131 |
+
|
| 132 |
+
console.log('🎨 Initializing advanced particle visualization with Entronaut SEFA...')
|
| 133 |
+
|
| 134 |
+
try {
|
| 135 |
+
// Dynamically import Three.js
|
| 136 |
+
const THREE = await import('three')
|
| 137 |
+
|
| 138 |
+
// Create scene
|
| 139 |
+
const scene = new THREE.Scene()
|
| 140 |
+
scene.background = new THREE.Color(0x000000)
|
| 141 |
+
sceneRef.current = scene
|
| 142 |
+
|
| 143 |
+
// Create camera
|
| 144 |
+
const camera = new THREE.PerspectiveCamera(
|
| 145 |
+
75,
|
| 146 |
+
window.innerWidth / window.innerHeight,
|
| 147 |
+
0.1,
|
| 148 |
+
1000
|
| 149 |
+
)
|
| 150 |
+
camera.position.set(0, 0, 2.5)
|
| 151 |
+
cameraRef.current = camera
|
| 152 |
+
|
| 153 |
+
// Create renderer
|
| 154 |
+
const renderer = new THREE.WebGLRenderer({
|
| 155 |
+
canvas: canvasRef.current!,
|
| 156 |
+
antialias: true
|
| 157 |
+
})
|
| 158 |
+
renderer.setSize(window.innerWidth, window.innerHeight)
|
| 159 |
+
rendererRef.current = renderer
|
| 160 |
+
|
| 161 |
+
// Initialize camera controls
|
| 162 |
+
const { OrbitControls } = await import('three/examples/jsm/controls/OrbitControls.js')
|
| 163 |
+
const controls = new OrbitControls(camera, renderer.domElement)
|
| 164 |
+
|
| 165 |
+
// Configure controls
|
| 166 |
+
controls.enableDamping = true
|
| 167 |
+
controls.dampingFactor = 0.05
|
| 168 |
+
controls.enableZoom = true
|
| 169 |
+
controls.enablePan = true
|
| 170 |
+
controls.enableRotate = true
|
| 171 |
+
|
| 172 |
+
// Set limits
|
| 173 |
+
controls.maxDistance = 50
|
| 174 |
+
controls.minDistance = 0.1
|
| 175 |
+
controls.maxPolarAngle = Math.PI // Allow full rotation
|
| 176 |
+
|
| 177 |
+
// Auto-rotate settings
|
| 178 |
+
controls.autoRotate = autoRotate
|
| 179 |
+
controls.autoRotateSpeed = 0.5
|
| 180 |
+
|
| 181 |
+
// Prevent controls from interfering with UI elements
|
| 182 |
+
controls.addEventListener('change', () => {
|
| 183 |
+
// Only update if camera controls are enabled
|
| 184 |
+
if (!cameraControlsEnabled) {
|
| 185 |
+
controls.enabled = false
|
| 186 |
+
}
|
| 187 |
+
})
|
| 188 |
+
|
| 189 |
+
controlsRef.current = controls
|
| 190 |
+
|
| 191 |
+
// Initialize noise generator and beat manager
|
| 192 |
+
noiseGeneratorRef.current = new NoiseGenerator()
|
| 193 |
+
beatManagerRef.current = new BeatManager()
|
| 194 |
+
|
| 195 |
+
// Initialize Entronaut
|
| 196 |
+
initializeEntronaut()
|
| 197 |
+
|
| 198 |
+
// Create particle spheres
|
| 199 |
+
await createParticleSpheres(THREE, scene)
|
| 200 |
+
|
| 201 |
+
// Create dynamic tendril system
|
| 202 |
+
if (tendrilsEnabled) {
|
| 203 |
+
await createTendrilSystem(THREE, scene)
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
// Create Dyson sphere/growing vines system
|
| 207 |
+
if (dysonSphereEnabled) {
|
| 208 |
+
await createDysonSphere(THREE, scene)
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
// Setup fog
|
| 212 |
+
updateFog(THREE)
|
| 213 |
+
|
| 214 |
+
setIsVisualizationReady(true)
|
| 215 |
+
console.log('✅ Advanced particle visualization with Entronaut initialized')
|
| 216 |
+
|
| 217 |
+
} catch (error) {
|
| 218 |
+
console.error('❌ Failed to initialize visualization:', error)
|
| 219 |
+
}
|
| 220 |
+
}, [initializeEntronaut])
|
| 221 |
+
|
| 222 |
+
// Create multiple particle sphere systems
|
| 223 |
+
const createParticleSpheres = useCallback(async (THREE: any, scene: any) => {
|
| 224 |
+
const spheres: ParticleSphere[] = []
|
| 225 |
+
|
| 226 |
+
// Load organic color schemes from config
|
| 227 |
+
const config = configLoader.getConfig()
|
| 228 |
+
const configColorSchemes = (config as any)?.colors?.sphereColorSchemes
|
| 229 |
+
|
| 230 |
+
// Enhanced organic color schemes for each sphere
|
| 231 |
+
const colorSchemes = configColorSchemes || [
|
| 232 |
+
{ start: '#2d5016', end: '#7d4f39' }, // Deep forest to iron oxide (sub-bass)
|
| 233 |
+
{ start: '#8b5a2b', end: '#4a6741' }, // Earth brown to vine green (bass)
|
| 234 |
+
{ start: '#cd853f', end: '#556b2f' }, // Sandy brown to moss green (mid)
|
| 235 |
+
{ start: '#daa520', end: '#2e8b57' }, // Gold leaf to sea green (high-mid)
|
| 236 |
+
{ start: '#b8860b', end: '#20b2aa' } // Burnished gold to teal depth (high)
|
| 237 |
+
]
|
| 238 |
+
|
| 239 |
+
for (let i = 0; i < 5; i++) {
|
| 240 |
+
const sphereParams: ParticleSystemParams = {
|
| 241 |
+
...DEFAULT_SPHERE_PARAMS,
|
| 242 |
+
enabled: true, // All spheres enabled by default
|
| 243 |
+
...DEFAULT_FREQUENCY_RANGES[i] || DEFAULT_FREQUENCY_RANGES[0],
|
| 244 |
+
colorStart: colorSchemes[i].start,
|
| 245 |
+
colorEnd: colorSchemes[i].end,
|
| 246 |
+
// Enhanced settings for organic, rich visuals
|
| 247 |
+
particleCount: 15000, // Increased for more density
|
| 248 |
+
turbulenceStrength: 0.006, // Reduced for more organic movement
|
| 249 |
+
beatStrength: 0.015, // Gentler beat response for organic feel
|
| 250 |
+
noiseScale: 2.5 + i * 0.3, // Smoother noise variation
|
| 251 |
+
sphereRadius: 1.0 + i * 0.06, // More subtle spatial variation
|
| 252 |
+
rotationSpeedMax: 0.04 + i * 0.006, // Slower, more organic rotation
|
| 253 |
+
particleLifetime: 12.0 + i * 2.0 // Longer lifetimes for stability
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
const sphere = await createParticleSphere(THREE, scene, i, sphereParams)
|
| 257 |
+
spheres.push(sphere)
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
spheresRef.current = spheres
|
| 261 |
+
console.log(`🔮 Created ${spheres.length} particle sphere systems`)
|
| 262 |
+
}, [])
|
| 263 |
+
|
| 264 |
+
// Create dynamic tendril system that connects particles
|
| 265 |
+
const createTendrilSystem = useCallback(async (THREE: any, scene: any) => {
|
| 266 |
+
if (!tendrilsEnabled) return
|
| 267 |
+
|
| 268 |
+
console.log('🌿 Creating dynamic tendril system...')
|
| 269 |
+
|
| 270 |
+
// Create tendril network between particles
|
| 271 |
+
const maxTendrils = 2000 // Maximum number of tendrils
|
| 272 |
+
const maxTendrilLength = 0.8 // Maximum distance for tendril connections
|
| 273 |
+
const tendrilSegments = 8 // Segments per tendril for smooth curves
|
| 274 |
+
|
| 275 |
+
// Create tendril geometry
|
| 276 |
+
const tendrilGeometry = new THREE.BufferGeometry()
|
| 277 |
+
const tendrilPositions = new Float32Array(maxTendrils * tendrilSegments * 6) // 2 points per segment
|
| 278 |
+
const tendrilColors = new Float32Array(maxTendrils * tendrilSegments * 6) // RGB for each point
|
| 279 |
+
const tendrilOpacities = new Float32Array(maxTendrils * tendrilSegments * 2) // Alpha for each point
|
| 280 |
+
|
| 281 |
+
// Initialize with empty data
|
| 282 |
+
tendrilPositions.fill(0)
|
| 283 |
+
tendrilColors.fill(0.3) // Dim gold base
|
| 284 |
+
tendrilOpacities.fill(0)
|
| 285 |
+
|
| 286 |
+
tendrilGeometry.setAttribute('position', new THREE.BufferAttribute(tendrilPositions, 3))
|
| 287 |
+
tendrilGeometry.setAttribute('color', new THREE.BufferAttribute(tendrilColors, 3))
|
| 288 |
+
tendrilGeometry.setAttribute('alpha', new THREE.BufferAttribute(tendrilOpacities, 1))
|
| 289 |
+
|
| 290 |
+
// Create custom shader material for tendrils
|
| 291 |
+
const tendrilMaterial = new THREE.ShaderMaterial({
|
| 292 |
+
uniforms: {
|
| 293 |
+
time: { value: 0.0 },
|
| 294 |
+
sefaField: { value: 0.0 },
|
| 295 |
+
audioEnergy: { value: 0.0 }
|
| 296 |
+
},
|
| 297 |
+
vertexShader: `
|
| 298 |
+
attribute float alpha;
|
| 299 |
+
varying float vAlpha;
|
| 300 |
+
varying vec3 vColor;
|
| 301 |
+
uniform float time;
|
| 302 |
+
uniform float sefaField;
|
| 303 |
+
uniform float audioEnergy;
|
| 304 |
+
|
| 305 |
+
void main() {
|
| 306 |
+
vAlpha = alpha * (0.3 + 0.7 * sin(time * 2.0 + position.x * 10.0));
|
| 307 |
+
vColor = color * (0.5 + 0.5 * audioEnergy);
|
| 308 |
+
|
| 309 |
+
vec3 pos = position;
|
| 310 |
+
// Add SEFA-driven undulation
|
| 311 |
+
pos += 0.02 * sefaField * sin(time * 3.0 + position.y * 15.0) * normal;
|
| 312 |
+
|
| 313 |
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
|
| 314 |
+
}
|
| 315 |
+
`,
|
| 316 |
+
fragmentShader: `
|
| 317 |
+
varying float vAlpha;
|
| 318 |
+
varying vec3 vColor;
|
| 319 |
+
|
| 320 |
+
void main() {
|
| 321 |
+
// Create glowing tendril effect
|
| 322 |
+
float glow = 1.0 - length(gl_PointCoord - vec2(0.5));
|
| 323 |
+
glow = pow(glow, 2.0);
|
| 324 |
+
|
| 325 |
+
gl_FragColor = vec4(vColor, vAlpha * glow);
|
| 326 |
+
}
|
| 327 |
+
`,
|
| 328 |
+
transparent: true,
|
| 329 |
+
blending: THREE.AdditiveBlending,
|
| 330 |
+
side: THREE.DoubleSide
|
| 331 |
+
})
|
| 332 |
+
|
| 333 |
+
// Create tendril line system
|
| 334 |
+
const tendrilLines = new THREE.LineSegments(tendrilGeometry, tendrilMaterial)
|
| 335 |
+
tendrilLines.userData = { type: 'tendrils' }
|
| 336 |
+
scene.add(tendrilLines)
|
| 337 |
+
|
| 338 |
+
tendrilSystemRef.current = {
|
| 339 |
+
geometry: tendrilGeometry,
|
| 340 |
+
material: tendrilMaterial,
|
| 341 |
+
lines: tendrilLines,
|
| 342 |
+
positions: tendrilPositions,
|
| 343 |
+
colors: tendrilColors,
|
| 344 |
+
opacities: tendrilOpacities,
|
| 345 |
+
maxTendrils,
|
| 346 |
+
maxTendrilLength,
|
| 347 |
+
tendrilSegments,
|
| 348 |
+
lastUpdate: 0
|
| 349 |
+
}
|
| 350 |
+
|
| 351 |
+
console.log('✅ Dynamic tendril system created')
|
| 352 |
+
}, [tendrilsEnabled])
|
| 353 |
+
|
| 354 |
+
// Create Dyson sphere/growing vines system
|
| 355 |
+
const createDysonSphere = useCallback(async (THREE: any, scene: any) => {
|
| 356 |
+
if (!dysonSphereEnabled) return
|
| 357 |
+
|
| 358 |
+
console.log('🌿 Creating Dyson sphere growing vines system...')
|
| 359 |
+
|
| 360 |
+
const config = configLoader.getConfig()
|
| 361 |
+
const dysonConfig = (config as any)?.dysonSphere
|
| 362 |
+
const colors = (config as any)?.colors?.dysonVines
|
| 363 |
+
|
| 364 |
+
if (!dysonConfig || !colors) {
|
| 365 |
+
console.warn('Dyson sphere configuration missing')
|
| 366 |
+
return
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
// Initialize vine system
|
| 370 |
+
const maxVines = vineComplexity
|
| 371 |
+
const maxSegments = dysonConfig.vines.maxSegmentsPerVine
|
| 372 |
+
const sphereRadius = dysonConfig.vines.sphereRadius
|
| 373 |
+
|
| 374 |
+
// Create vine geometry
|
| 375 |
+
const vineGeometry = new THREE.BufferGeometry()
|
| 376 |
+
const vinePositions = new Float32Array(maxVines * maxSegments * 6) // Line segments
|
| 377 |
+
const vineColors = new Float32Array(maxVines * maxSegments * 6)
|
| 378 |
+
const vineOpacities = new Float32Array(maxVines * maxSegments * 2)
|
| 379 |
+
|
| 380 |
+
// Initialize with empty data
|
| 381 |
+
vinePositions.fill(0)
|
| 382 |
+
vineColors.fill(0)
|
| 383 |
+
vineOpacities.fill(0)
|
| 384 |
+
|
| 385 |
+
vineGeometry.setAttribute('position', new THREE.BufferAttribute(vinePositions, 3))
|
| 386 |
+
vineGeometry.setAttribute('color', new THREE.BufferAttribute(vineColors, 3))
|
| 387 |
+
vineGeometry.setAttribute('alpha', new THREE.BufferAttribute(vineOpacities, 1))
|
| 388 |
+
|
| 389 |
+
// Create node geometry for connection points
|
| 390 |
+
const nodeGeometry = new THREE.SphereGeometry(dysonConfig.nodes.nodeSize, 8, 6)
|
| 391 |
+
const nodeMaterial = new THREE.MeshBasicMaterial({
|
| 392 |
+
color: new THREE.Color(colors.nodeColor),
|
| 393 |
+
transparent: true,
|
| 394 |
+
opacity: 0.8
|
| 395 |
+
})
|
| 396 |
+
|
| 397 |
+
// Create vine shader material
|
| 398 |
+
const vineMaterial = new THREE.ShaderMaterial({
|
| 399 |
+
uniforms: {
|
| 400 |
+
time: { value: 0.0 },
|
| 401 |
+
growthProgress: { value: 0.0 },
|
| 402 |
+
audioEnergy: { value: 0.0 },
|
| 403 |
+
emergenceField: { value: 0.0 },
|
| 404 |
+
vineGlow: { value: dysonConfig.visual.vineGlow },
|
| 405 |
+
primaryColor: { value: new THREE.Color(colors.primaryVine) },
|
| 406 |
+
secondaryColor: { value: new THREE.Color(colors.secondaryVine) },
|
| 407 |
+
highlightColor: { value: new THREE.Color(colors.vineHighlight) }
|
| 408 |
+
},
|
| 409 |
+
vertexShader: `
|
| 410 |
+
attribute float alpha;
|
| 411 |
+
varying float vAlpha;
|
| 412 |
+
varying vec3 vColor;
|
| 413 |
+
varying vec3 vPosition;
|
| 414 |
+
uniform float time;
|
| 415 |
+
uniform float growthProgress;
|
| 416 |
+
uniform float audioEnergy;
|
| 417 |
+
uniform vec3 primaryColor;
|
| 418 |
+
uniform vec3 secondaryColor;
|
| 419 |
+
uniform vec3 highlightColor;
|
| 420 |
+
|
| 421 |
+
void main() {
|
| 422 |
+
vAlpha = alpha * (0.4 + 0.6 * sin(time * 2.0 + position.x * 8.0));
|
| 423 |
+
vPosition = position;
|
| 424 |
+
|
| 425 |
+
// Organic color blending
|
| 426 |
+
float colorMix = sin(time * 1.5 + position.y * 6.0) * 0.5 + 0.5;
|
| 427 |
+
vColor = mix(primaryColor, secondaryColor, colorMix);
|
| 428 |
+
vColor = mix(vColor, highlightColor, audioEnergy * 0.3);
|
| 429 |
+
|
| 430 |
+
vec3 pos = position;
|
| 431 |
+
// Add organic growth undulation
|
| 432 |
+
float growth = min(1.0, growthProgress + sin(time * 0.5) * 0.1);
|
| 433 |
+
pos *= growth;
|
| 434 |
+
|
| 435 |
+
// Audio-reactive pulsing
|
| 436 |
+
pos += 0.01 * audioEnergy * sin(time * 4.0 + length(position) * 12.0) * normalize(position);
|
| 437 |
+
|
| 438 |
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
|
| 439 |
+
}
|
| 440 |
+
`,
|
| 441 |
+
fragmentShader: `
|
| 442 |
+
varying float vAlpha;
|
| 443 |
+
varying vec3 vColor;
|
| 444 |
+
varying vec3 vPosition;
|
| 445 |
+
uniform float vineGlow;
|
| 446 |
+
|
| 447 |
+
void main() {
|
| 448 |
+
// Distance-based glow effect
|
| 449 |
+
float dist = length(vPosition);
|
| 450 |
+
float glow = 1.0 / (1.0 + dist * 2.0);
|
| 451 |
+
glow = pow(glow, vineGlow);
|
| 452 |
+
|
| 453 |
+
// Organic pulsing opacity
|
| 454 |
+
float pulse = sin(dist * 8.0 - time * 3.0) * 0.3 + 0.7;
|
| 455 |
+
|
| 456 |
+
gl_FragColor = vec4(vColor * glow, vAlpha * pulse);
|
| 457 |
+
}
|
| 458 |
+
`,
|
| 459 |
+
transparent: true,
|
| 460 |
+
blending: THREE.AdditiveBlending,
|
| 461 |
+
side: THREE.DoubleSide
|
| 462 |
+
})
|
| 463 |
+
|
| 464 |
+
// Create vine line system
|
| 465 |
+
const vineLines = new THREE.LineSegments(vineGeometry, vineMaterial)
|
| 466 |
+
vineLines.userData = { type: 'dysonVines' }
|
| 467 |
+
scene.add(vineLines)
|
| 468 |
+
|
| 469 |
+
// Create node instances
|
| 470 |
+
const nodeInstances = []
|
| 471 |
+
for (let i = 0; i < dysonConfig.nodes.nodeCount; i++) {
|
| 472 |
+
const node = new THREE.Mesh(nodeGeometry, nodeMaterial.clone())
|
| 473 |
+
node.userData = { type: 'dysonNode', index: i }
|
| 474 |
+
scene.add(node)
|
| 475 |
+
nodeInstances.push(node)
|
| 476 |
+
}
|
| 477 |
+
|
| 478 |
+
// Initialize vine growth data
|
| 479 |
+
const vineData = {
|
| 480 |
+
vines: [] as any[],
|
| 481 |
+
growthProgress: 0,
|
| 482 |
+
lastUpdate: 0
|
| 483 |
+
}
|
| 484 |
+
|
| 485 |
+
// Create initial vine seeds
|
| 486 |
+
for (let i = 0; i < dysonConfig.growth.seedPoints; i++) {
|
| 487 |
+
const theta = (i / dysonConfig.growth.seedPoints) * Math.PI * 2
|
| 488 |
+
const phi = Math.acos(2 * Math.random() - 1)
|
| 489 |
+
|
| 490 |
+
const startPos = {
|
| 491 |
+
x: sphereRadius * Math.sin(phi) * Math.cos(theta),
|
| 492 |
+
y: sphereRadius * Math.sin(phi) * Math.sin(theta),
|
| 493 |
+
z: sphereRadius * Math.cos(phi)
|
| 494 |
+
}
|
| 495 |
+
|
| 496 |
+
vineData.vines.push({
|
| 497 |
+
segments: [startPos],
|
| 498 |
+
growth: 0,
|
| 499 |
+
direction: { x: Math.random() - 0.5, y: Math.random() - 0.5, z: Math.random() - 0.5 },
|
| 500 |
+
branches: [],
|
| 501 |
+
maturity: 0,
|
| 502 |
+
energy: Math.random()
|
| 503 |
+
})
|
| 504 |
+
}
|
| 505 |
+
|
| 506 |
+
dysonSphereRef.current = {
|
| 507 |
+
vineGeometry,
|
| 508 |
+
vineMaterial,
|
| 509 |
+
vineLines,
|
| 510 |
+
nodeGeometry,
|
| 511 |
+
nodeInstances,
|
| 512 |
+
vinePositions,
|
| 513 |
+
vineColors,
|
| 514 |
+
vineOpacities,
|
| 515 |
+
vineData,
|
| 516 |
+
maxVines,
|
| 517 |
+
maxSegments,
|
| 518 |
+
sphereRadius,
|
| 519 |
+
lastUpdate: 0,
|
| 520 |
+
config: dysonConfig,
|
| 521 |
+
colors
|
| 522 |
+
}
|
| 523 |
+
|
| 524 |
+
console.log('✅ Dyson sphere growing vines system created')
|
| 525 |
+
}, [dysonSphereEnabled, vineComplexity])
|
| 526 |
+
|
| 527 |
+
// Create individual particle sphere
|
| 528 |
+
const createParticleSphere = async (
|
| 529 |
+
THREE: any,
|
| 530 |
+
scene: any,
|
| 531 |
+
index: number,
|
| 532 |
+
params: ParticleSystemParams
|
| 533 |
+
): Promise<ParticleSphere> => {
|
| 534 |
+
|
| 535 |
+
// Create geometry and buffers
|
| 536 |
+
const geometry = new THREE.BufferGeometry()
|
| 537 |
+
const positions = new Float32Array(params.particleCount * 3)
|
| 538 |
+
const colors = new Float32Array(params.particleCount * 3)
|
| 539 |
+
const velocities = new Float32Array(params.particleCount * 3)
|
| 540 |
+
const basePositions = new Float32Array(params.particleCount * 3)
|
| 541 |
+
const lifetimes = new Float32Array(params.particleCount)
|
| 542 |
+
const maxLifetimes = new Float32Array(params.particleCount)
|
| 543 |
+
const beatEffects = new Float32Array(params.particleCount)
|
| 544 |
+
|
| 545 |
+
// Initialize particles
|
| 546 |
+
for (let i = 0; i < params.particleCount; i++) {
|
| 547 |
+
const i3 = i * 3
|
| 548 |
+
const radius = THREE.MathUtils.lerp(0, params.sphereRadius, params.innerSphereRadius)
|
| 549 |
+
const theta = Math.random() * Math.PI * 2
|
| 550 |
+
const phi = Math.acos(2 * Math.random() - 1)
|
| 551 |
+
const r = Math.cbrt(Math.random()) * radius
|
| 552 |
+
|
| 553 |
+
const x = r * Math.sin(phi) * Math.cos(theta)
|
| 554 |
+
const y = r * Math.sin(phi) * Math.sin(theta)
|
| 555 |
+
const z = r * Math.cos(phi)
|
| 556 |
+
|
| 557 |
+
positions[i3] = x
|
| 558 |
+
positions[i3 + 1] = y
|
| 559 |
+
positions[i3 + 2] = z
|
| 560 |
+
|
| 561 |
+
basePositions[i3] = x
|
| 562 |
+
basePositions[i3 + 1] = y
|
| 563 |
+
basePositions[i3 + 2] = z
|
| 564 |
+
|
| 565 |
+
velocities[i3] = 0
|
| 566 |
+
velocities[i3 + 1] = 0
|
| 567 |
+
velocities[i3 + 2] = 0
|
| 568 |
+
|
| 569 |
+
const lt = Math.random() * params.particleLifetime
|
| 570 |
+
lifetimes[i] = lt
|
| 571 |
+
maxLifetimes[i] = lt
|
| 572 |
+
beatEffects[i] = 0
|
| 573 |
+
}
|
| 574 |
+
|
| 575 |
+
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3))
|
| 576 |
+
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3))
|
| 577 |
+
|
| 578 |
+
// Create material with organic styling
|
| 579 |
+
const material = new THREE.PointsMaterial({
|
| 580 |
+
size: params.particleSize * (0.8 + index * 0.05), // Subtle size variation per sphere
|
| 581 |
+
vertexColors: true,
|
| 582 |
+
transparent: true,
|
| 583 |
+
opacity: 0.85 - index * 0.05, // Decreasing opacity for depth
|
| 584 |
+
blending: THREE.AdditiveBlending,
|
| 585 |
+
fog: true,
|
| 586 |
+
sizeAttenuation: true // Natural size falloff with distance
|
| 587 |
+
})
|
| 588 |
+
|
| 589 |
+
// Create particle system
|
| 590 |
+
const particleSystem = new THREE.Points(geometry, material)
|
| 591 |
+
particleSystem.visible = params.enabled
|
| 592 |
+
particleSystem.userData = { sphereIndex: index } // Critical: Link to sphere
|
| 593 |
+
scene.add(particleSystem)
|
| 594 |
+
|
| 595 |
+
// Create sphere object
|
| 596 |
+
const sphere: ParticleSphere = {
|
| 597 |
+
index,
|
| 598 |
+
params,
|
| 599 |
+
positions,
|
| 600 |
+
velocities,
|
| 601 |
+
basePositions,
|
| 602 |
+
lifetimes,
|
| 603 |
+
maxLifetimes,
|
| 604 |
+
beatEffects,
|
| 605 |
+
colors,
|
| 606 |
+
lastNoiseScale: params.noiseScale,
|
| 607 |
+
lastValidVolume: 0,
|
| 608 |
+
lastRotationSpeed: 0,
|
| 609 |
+
peakDetection: {
|
| 610 |
+
energyHistory: [],
|
| 611 |
+
historyLength: 30,
|
| 612 |
+
lastPeakTime: 0,
|
| 613 |
+
minTimeBetweenPeaks: 200
|
| 614 |
+
}
|
| 615 |
+
}
|
| 616 |
+
|
| 617 |
+
// Update colors
|
| 618 |
+
updateSphereColors(THREE, sphere)
|
| 619 |
+
|
| 620 |
+
return sphere
|
| 621 |
+
}
|
| 622 |
+
|
| 623 |
+
// Update sphere colors with organic palette blending
|
| 624 |
+
const updateSphereColors = (THREE: any, sphere: ParticleSphere) => {
|
| 625 |
+
const color1 = new THREE.Color(sphere.params.colorStart)
|
| 626 |
+
const color2 = new THREE.Color(sphere.params.colorEnd)
|
| 627 |
+
|
| 628 |
+
// Load organic palette for enhanced blending
|
| 629 |
+
const config = configLoader.getConfig()
|
| 630 |
+
const organicPalette = (config as any)?.colors?.organicPalette
|
| 631 |
+
|
| 632 |
+
for (let i = 0; i < sphere.params.particleCount; i++) {
|
| 633 |
+
const t = i / sphere.params.particleCount
|
| 634 |
+
|
| 635 |
+
// Base color interpolation
|
| 636 |
+
let r = color1.r * (1 - t) + color2.r * t
|
| 637 |
+
let g = color1.g * (1 - t) + color2.g * t
|
| 638 |
+
let b = color1.b * (1 - t) + color2.b * t
|
| 639 |
+
|
| 640 |
+
// Apply organic palette enhancement if available
|
| 641 |
+
if (organicPalette && entronautEnabled) {
|
| 642 |
+
const emergence = entronautStateRef.current?.informationMetrics?.emergence || 0
|
| 643 |
+
|
| 644 |
+
// Blend with organic colors based on SEFA emergence
|
| 645 |
+
const organicInfluence = emergence * 0.3
|
| 646 |
+
|
| 647 |
+
// Choose organic color based on frequency range
|
| 648 |
+
let organicColor
|
| 649 |
+
if (sphere.index === 0) { // Sub-bass -> deep forest/bronze
|
| 650 |
+
organicColor = hexToRgb(organicPalette.deepForest) || { r: 0.18, g: 0.31, b: 0.09 }
|
| 651 |
+
} else if (sphere.index === 1) { // Bass -> earth brown/copper
|
| 652 |
+
organicColor = hexToRgb(organicPalette.earthBrown) || { r: 0.54, g: 0.27, b: 0.07 }
|
| 653 |
+
} else if (sphere.index === 2) { // Mid -> copper glow/gold
|
| 654 |
+
organicColor = hexToRgb(organicPalette.copperGlow) || { r: 0.72, g: 0.45, b: 0.20 }
|
| 655 |
+
} else if (sphere.index === 3) { // High-mid -> gold leaf/moss
|
| 656 |
+
organicColor = hexToRgb(organicPalette.goldLeaf) || { r: 0.85, g: 0.65, b: 0.13 }
|
| 657 |
+
} else { // High -> teal depth/amber
|
| 658 |
+
organicColor = hexToRgb(organicPalette.tealDepth) || { r: 0.13, g: 0.70, b: 0.67 }
|
| 659 |
+
}
|
| 660 |
+
|
| 661 |
+
// Blend with organic color
|
| 662 |
+
r = r * (1 - organicInfluence) + organicColor.r * organicInfluence
|
| 663 |
+
g = g * (1 - organicInfluence) + organicColor.g * organicInfluence
|
| 664 |
+
b = b * (1 - organicInfluence) + organicColor.b * organicInfluence
|
| 665 |
+
}
|
| 666 |
+
|
| 667 |
+
sphere.colors[i * 3] = r
|
| 668 |
+
sphere.colors[i * 3 + 1] = g
|
| 669 |
+
sphere.colors[i * 3 + 2] = b
|
| 670 |
+
}
|
| 671 |
+
}
|
| 672 |
+
|
| 673 |
+
// Update fog
|
| 674 |
+
const updateFog = (THREE: any) => {
|
| 675 |
+
if (!sceneRef.current) return
|
| 676 |
+
|
| 677 |
+
if (!fogParams.enabled) {
|
| 678 |
+
sceneRef.current.fog = null
|
| 679 |
+
} else {
|
| 680 |
+
const color = new THREE.Color(fogParams.color)
|
| 681 |
+
sceneRef.current.fog = new THREE.Fog(color, fogParams.near, fogParams.far)
|
| 682 |
+
}
|
| 683 |
+
}
|
| 684 |
+
|
| 685 |
+
// Main animation loop
|
| 686 |
+
const animate = useCallback((currentTime: number) => {
|
| 687 |
+
if (!isVisualizationReady || !rendererRef.current || !sceneRef.current || !cameraRef.current) {
|
| 688 |
+
animationRef.current = requestAnimationFrame(animate)
|
| 689 |
+
return
|
| 690 |
+
}
|
| 691 |
+
|
| 692 |
+
const deltaTime = lastTimeRef.current ? (currentTime - lastTimeRef.current) / 1000 : 0
|
| 693 |
+
lastTimeRef.current = currentTime
|
| 694 |
+
|
| 695 |
+
// Simple FPS monitoring - no complex optimization
|
| 696 |
+
setPerformanceStats(prev => {
|
| 697 |
+
const frameCount = prev.frameCount + 1
|
| 698 |
+
const timeSinceLastUpdate = currentTime - prev.lastFpsUpdate
|
| 699 |
+
|
| 700 |
+
if (timeSinceLastUpdate > 1000) { // Update FPS every second
|
| 701 |
+
const fps = Math.round(frameCount * 1000 / timeSinceLastUpdate)
|
| 702 |
+
const totalParticles = spheresRef.current.reduce((sum, sphere) =>
|
| 703 |
+
sum + (sphere.params.enabled ? sphere.params.particleCount : 0), 0)
|
| 704 |
+
|
| 705 |
+
return {
|
| 706 |
+
fps,
|
| 707 |
+
particleCount: totalParticles,
|
| 708 |
+
lastFpsUpdate: currentTime,
|
| 709 |
+
frameCount: 0
|
| 710 |
+
}
|
| 711 |
+
}
|
| 712 |
+
|
| 713 |
+
return { ...prev, frameCount }
|
| 714 |
+
})
|
| 715 |
+
|
| 716 |
+
// Update camera controls
|
| 717 |
+
if (controlsRef.current) {
|
| 718 |
+
controlsRef.current.enabled = cameraControlsEnabled
|
| 719 |
+
controlsRef.current.autoRotate = autoRotate && cameraControlsEnabled
|
| 720 |
+
if (cameraControlsEnabled) {
|
| 721 |
+
controlsRef.current.update()
|
| 722 |
+
}
|
| 723 |
+
}
|
| 724 |
+
|
| 725 |
+
// Update beat manager
|
| 726 |
+
beatManagerRef.current?.update(deltaTime)
|
| 727 |
+
|
| 728 |
+
// ENTRONAUT: Analyze emergence patterns in audio data (reduced frequency for performance)
|
| 729 |
+
if (audioEngine && entronautEnabled && (Math.floor(currentTime * 0.01) % 3 === 0)) { // Run every 3rd frame
|
| 730 |
+
const audioData = audioEngine.getAudioData()
|
| 731 |
+
if (Math.random() < 0.005) { // Reduced logging frequency
|
| 732 |
+
console.log('🧠 SEFA Analysis - Audio Data:', {
|
| 733 |
+
deepEnergy: audioData.deepEnergy,
|
| 734 |
+
midEnergy: audioData.midEnergy,
|
| 735 |
+
highEnergy: audioData.highEnergy,
|
| 736 |
+
overallAmplitude: audioData.overallAmplitude,
|
| 737 |
+
beatDetected: audioData.beatDetected
|
| 738 |
+
})
|
| 739 |
+
}
|
| 740 |
+
analyzeSymbolicEmergence(audioData, currentTime)
|
| 741 |
+
}
|
| 742 |
+
|
| 743 |
+
// Update particle spheres
|
| 744 |
+
updateParticleSpheres(currentTime, deltaTime)
|
| 745 |
+
|
| 746 |
+
// Update dynamic tendrils
|
| 747 |
+
updateTendrilSystem(currentTime, deltaTime)
|
| 748 |
+
|
| 749 |
+
// Update Dyson sphere/growing vines
|
| 750 |
+
updateDysonSphere(currentTime, deltaTime)
|
| 751 |
+
|
| 752 |
+
// Render scene
|
| 753 |
+
rendererRef.current.render(sceneRef.current, cameraRef.current)
|
| 754 |
+
|
| 755 |
+
animationRef.current = requestAnimationFrame(animate)
|
| 756 |
+
}, [isVisualizationReady, audioEngine, entronautEnabled, analyzeSymbolicEmergence])
|
| 757 |
+
|
| 758 |
+
// Update all particle spheres with inter-sphere communication
|
| 759 |
+
const updateParticleSpheres = (currentTime: number, deltaTime: number) => {
|
| 760 |
+
if (!audioEngine || !noiseGeneratorRef.current || !beatManagerRef.current) return
|
| 761 |
+
|
| 762 |
+
// Calculate global emergence state for inter-sphere communication
|
| 763 |
+
const globalEmergence = entronautStateRef.current ?
|
| 764 |
+
entronautStateRef.current.informationMetrics.emergence : 0
|
| 765 |
+
const globalComplexity = entronautStateRef.current ?
|
| 766 |
+
entronautStateRef.current.informationMetrics.complexity : 0
|
| 767 |
+
|
| 768 |
+
spheresRef.current.forEach((sphere, sphereIndex) => {
|
| 769 |
+
if (!sphere.params.enabled) return
|
| 770 |
+
|
| 771 |
+
// Get audio data for this sphere
|
| 772 |
+
const audioData = audioEngine.getAdvancedAudioData(sphere)
|
| 773 |
+
|
| 774 |
+
// Add debug logging for the first sphere
|
| 775 |
+
if (sphere.index === 0 && Math.random() < 0.01) { // 1% chance to log
|
| 776 |
+
console.log('🎯 Audio data for sphere 0:', {
|
| 777 |
+
rangeEnergy: audioData.rangeEnergy,
|
| 778 |
+
rangeEnergyBeat: audioData.rangeEnergyBeat,
|
| 779 |
+
peakDetected: audioData.peakDetected,
|
| 780 |
+
beatThreshold: sphere.params.beatThreshold
|
| 781 |
+
})
|
| 782 |
+
}
|
| 783 |
+
|
| 784 |
+
// OPTIMIZED INTER-SPHERE COMMUNICATION (run less frequently)
|
| 785 |
+
if (entronautEnabled && adaptiveCoupling && (sphereIndex === 0 || Math.random() < 0.1)) {
|
| 786 |
+
// Simplified sphere influence with reduced frequency
|
| 787 |
+
let avgNoiseScale = 0
|
| 788 |
+
let activeNeighbors = 0
|
| 789 |
+
|
| 790 |
+
spheresRef.current.forEach((otherSphere, otherIndex) => {
|
| 791 |
+
if (otherIndex !== sphereIndex && otherSphere.params.enabled) {
|
| 792 |
+
avgNoiseScale += otherSphere.params.noiseScale
|
| 793 |
+
activeNeighbors++
|
| 794 |
+
}
|
| 795 |
+
})
|
| 796 |
+
|
| 797 |
+
if (activeNeighbors > 0) {
|
| 798 |
+
avgNoiseScale /= activeNeighbors
|
| 799 |
+
const networkInfluence = globalEmergence * 0.1 // Reduced influence
|
| 800 |
+
sphere.params.noiseScale = sphere.params.noiseScale * (1 - networkInfluence) +
|
| 801 |
+
avgNoiseScale * networkInfluence
|
| 802 |
+
}
|
| 803 |
+
|
| 804 |
+
// Simplified synchronization
|
| 805 |
+
const syncPulse = globalComplexity * 0.2
|
| 806 |
+
sphere.params.turbulenceStrength *= (1 + syncPulse)
|
| 807 |
+
}
|
| 808 |
+
|
| 809 |
+
// Handle peak detection and dynamic noise scaling
|
| 810 |
+
if (audioData.peakDetected && sphere.params.dynamicNoiseScale) {
|
| 811 |
+
sphere.params.noiseScale = generateNewNoiseScale(sphere.params, sphere.lastNoiseScale)
|
| 812 |
+
sphere.lastNoiseScale = sphere.params.noiseScale
|
| 813 |
+
}
|
| 814 |
+
|
| 815 |
+
// Beat detection and wave triggering
|
| 816 |
+
const beatDetected = audioData.rangeEnergyBeat > sphere.params.beatThreshold
|
| 817 |
+
if (beatDetected && !beatManagerRef.current!.isWaveActive && sphere.params.beatStrength > 0) {
|
| 818 |
+
beatManagerRef.current!.triggerWave(audioData.rangeEnergyBeat)
|
| 819 |
+
}
|
| 820 |
+
|
| 821 |
+
// Update particles
|
| 822 |
+
updateSphereParticles(sphere, currentTime, deltaTime, beatDetected)
|
| 823 |
+
|
| 824 |
+
// Update rotation based on audio
|
| 825 |
+
updateSphereRotation(sphere, audioData)
|
| 826 |
+
})
|
| 827 |
+
}
|
| 828 |
+
|
| 829 |
+
// Update dynamic tendril system with performance optimization
|
| 830 |
+
const updateTendrilSystem = (currentTime: number, _deltaTime: number) => {
|
| 831 |
+
if (!tendrilsEnabled || !tendrilSystemRef.current || !audioEngine || !entronautStateRef.current) return
|
| 832 |
+
|
| 833 |
+
// Performance optimization for tendril system
|
| 834 |
+
const adaptiveLevel = 1.0 // Simplified - no performance monitoring
|
| 835 |
+
|
| 836 |
+
const tendrilSystem = tendrilSystemRef.current
|
| 837 |
+
const { positions, colors, opacities, maxTendrils, maxTendrilLength, tendrilSegments } = tendrilSystem
|
| 838 |
+
|
| 839 |
+
// Update shader uniforms
|
| 840 |
+
const audioData = audioEngine.getAudioData()
|
| 841 |
+
const sefaMetrics = entronautStateRef.current.informationMetrics
|
| 842 |
+
|
| 843 |
+
tendrilSystem.material.uniforms.time.value = currentTime * 0.001
|
| 844 |
+
tendrilSystem.material.uniforms.sefaField.value = sefaMetrics.emergence
|
| 845 |
+
tendrilSystem.material.uniforms.audioEnergy.value = audioData.overallAmplitude || 0
|
| 846 |
+
|
| 847 |
+
// Only update connections periodically for performance
|
| 848 |
+
if (currentTime - tendrilSystem.lastUpdate < 100) return // Update every 100ms
|
| 849 |
+
tendrilSystem.lastUpdate = currentTime
|
| 850 |
+
|
| 851 |
+
let tendrilIndex = 0
|
| 852 |
+
const performanceMultiplier = Math.max(0.2, adaptiveLevel) // Minimum 20% tendrils even at low performance
|
| 853 |
+
const activeTendrils = Math.floor(maxTendrils * tendrilDensity * performanceMultiplier * (0.5 + 0.5 * audioData.overallAmplitude))
|
| 854 |
+
|
| 855 |
+
// Reset all positions
|
| 856 |
+
positions.fill(0)
|
| 857 |
+
opacities.fill(0)
|
| 858 |
+
|
| 859 |
+
// Create connections between particles from different spheres
|
| 860 |
+
for (let sphereA = 0; sphereA < spheresRef.current.length && tendrilIndex < activeTendrils; sphereA++) {
|
| 861 |
+
const sphere1 = spheresRef.current[sphereA]
|
| 862 |
+
if (!sphere1.params.enabled) continue
|
| 863 |
+
|
| 864 |
+
for (let sphereB = sphereA + 1; sphereB < spheresRef.current.length && tendrilIndex < activeTendrils; sphereB++) {
|
| 865 |
+
const sphere2 = spheresRef.current[sphereB]
|
| 866 |
+
if (!sphere2.params.enabled) continue
|
| 867 |
+
|
| 868 |
+
// Sample particles from each sphere
|
| 869 |
+
const sampleCount = Math.min(10, Math.floor(sphere1.params.particleCount / 1000))
|
| 870 |
+
|
| 871 |
+
for (let i = 0; i < sampleCount && tendrilIndex < activeTendrils; i++) {
|
| 872 |
+
const p1Index = Math.floor(Math.random() * sphere1.params.particleCount)
|
| 873 |
+
const p2Index = Math.floor(Math.random() * sphere2.params.particleCount)
|
| 874 |
+
|
| 875 |
+
const p1x = sphere1.positions[p1Index * 3]
|
| 876 |
+
const p1y = sphere1.positions[p1Index * 3 + 1]
|
| 877 |
+
const p1z = sphere1.positions[p1Index * 3 + 2]
|
| 878 |
+
|
| 879 |
+
const p2x = sphere2.positions[p2Index * 3]
|
| 880 |
+
const p2y = sphere2.positions[p2Index * 3 + 1]
|
| 881 |
+
const p2z = sphere2.positions[p2Index * 3 + 2]
|
| 882 |
+
|
| 883 |
+
const distance = Math.sqrt(
|
| 884 |
+
(p2x - p1x) ** 2 + (p2y - p1y) ** 2 + (p2z - p1z) ** 2
|
| 885 |
+
)
|
| 886 |
+
|
| 887 |
+
// Only create tendril if particles are within range
|
| 888 |
+
if (distance < maxTendrilLength) {
|
| 889 |
+
// Calculate SEFA influence on tendril strength
|
| 890 |
+
const fieldWidth = 32
|
| 891 |
+
const fieldX = Math.floor(((p1x + p2x) * 0.5 + 1) * 0.5 * fieldWidth)
|
| 892 |
+
const fieldY = Math.floor(((p1y + p2y) * 0.5 + 1) * 0.5 * fieldWidth)
|
| 893 |
+
const sefaIndex = Math.max(0, Math.min(
|
| 894 |
+
entronautStateRef.current.sefaScore.length - 1,
|
| 895 |
+
fieldY * fieldWidth + fieldX
|
| 896 |
+
))
|
| 897 |
+
const sefaStrength = entronautStateRef.current.sefaScore[sefaIndex]
|
| 898 |
+
|
| 899 |
+
// Create curved tendril with multiple segments
|
| 900 |
+
for (let segment = 0; segment < tendrilSegments - 1; segment++) {
|
| 901 |
+
const t1 = segment / (tendrilSegments - 1)
|
| 902 |
+
const t2 = (segment + 1) / (tendrilSegments - 1)
|
| 903 |
+
|
| 904 |
+
// Calculate curved path with SEFA-driven undulation
|
| 905 |
+
const curve1 = getCurvedTendrilPoint(p1x, p1y, p1z, p2x, p2y, p2z, t1, currentTime, sefaStrength)
|
| 906 |
+
const curve2 = getCurvedTendrilPoint(p1x, p1y, p1z, p2x, p2y, p2z, t2, currentTime, sefaStrength)
|
| 907 |
+
|
| 908 |
+
const baseIndex = tendrilIndex * tendrilSegments * 6 + segment * 6
|
| 909 |
+
|
| 910 |
+
// Set positions for line segment
|
| 911 |
+
positions[baseIndex] = curve1.x
|
| 912 |
+
positions[baseIndex + 1] = curve1.y
|
| 913 |
+
positions[baseIndex + 2] = curve1.z
|
| 914 |
+
positions[baseIndex + 3] = curve2.x
|
| 915 |
+
positions[baseIndex + 4] = curve2.y
|
| 916 |
+
positions[baseIndex + 5] = curve2.z
|
| 917 |
+
|
| 918 |
+
// Set colors based on frequency and SEFA with organic palette blending
|
| 919 |
+
const colorIntensity = 0.4 + 0.6 * sefaStrength // More subtle intensity range
|
| 920 |
+
const freq1Color = getFrequencyColor(sphere1.params.minFrequency, colorIntensity)
|
| 921 |
+
const freq2Color = getFrequencyColor(sphere2.params.minFrequency, colorIntensity)
|
| 922 |
+
|
| 923 |
+
// Blend colors organically
|
| 924 |
+
const blendFactor = Math.sin(currentTime * 0.0008 + distance * 5) * 0.5 + 0.5
|
| 925 |
+
|
| 926 |
+
colors[baseIndex] = freq1Color.r * (1 - blendFactor) + freq2Color.r * blendFactor
|
| 927 |
+
colors[baseIndex + 1] = freq1Color.g * (1 - blendFactor) + freq2Color.g * blendFactor
|
| 928 |
+
colors[baseIndex + 2] = freq1Color.b * (1 - blendFactor) + freq2Color.b * blendFactor
|
| 929 |
+
colors[baseIndex + 3] = freq2Color.r * (1 - blendFactor) + freq1Color.r * blendFactor
|
| 930 |
+
colors[baseIndex + 4] = freq2Color.g * (1 - blendFactor) + freq1Color.g * blendFactor
|
| 931 |
+
colors[baseIndex + 5] = freq2Color.b * (1 - blendFactor) + freq1Color.b * blendFactor
|
| 932 |
+
|
| 933 |
+
// Set opacity based on audio energy and distance
|
| 934 |
+
const opacity = (1 - distance / maxTendrilLength) * sefaStrength * audioData.overallAmplitude
|
| 935 |
+
const opacityIndex = tendrilIndex * tendrilSegments * 2 + segment * 2
|
| 936 |
+
opacities[opacityIndex] = opacity
|
| 937 |
+
opacities[opacityIndex + 1] = opacity
|
| 938 |
+
}
|
| 939 |
+
|
| 940 |
+
tendrilIndex++
|
| 941 |
+
}
|
| 942 |
+
}
|
| 943 |
+
}
|
| 944 |
+
}
|
| 945 |
+
|
| 946 |
+
// Mark geometry for update
|
| 947 |
+
tendrilSystem.geometry.attributes.position.needsUpdate = true
|
| 948 |
+
tendrilSystem.geometry.attributes.color.needsUpdate = true
|
| 949 |
+
tendrilSystem.geometry.attributes.alpha.needsUpdate = true
|
| 950 |
+
}
|
| 951 |
+
|
| 952 |
+
// Update Dyson sphere/growing vines system with performance optimization
|
| 953 |
+
const updateDysonSphere = (currentTime: number, deltaTime: number) => {
|
| 954 |
+
if (!dysonSphereEnabled || !dysonSphereRef.current || !audioEngine) return
|
| 955 |
+
|
| 956 |
+
// Performance optimization for Dyson sphere
|
| 957 |
+
// Simplified - no dynamic update frequency
|
| 958 |
+
|
| 959 |
+
const dysonSystem = dysonSphereRef.current
|
| 960 |
+
const { vineMaterial, vinePositions, vineColors, vineOpacities, vineData, config, colors } = dysonSystem
|
| 961 |
+
|
| 962 |
+
// Update shader uniforms
|
| 963 |
+
const audioData = audioEngine.getAudioData()
|
| 964 |
+
const sefaMetrics = entronautStateRef.current?.informationMetrics || { emergence: 0, complexity: 0 }
|
| 965 |
+
|
| 966 |
+
vineMaterial.uniforms.time.value = currentTime * 0.001
|
| 967 |
+
vineMaterial.uniforms.audioEnergy.value = audioData.overallAmplitude || 0
|
| 968 |
+
vineMaterial.uniforms.emergenceField.value = sefaMetrics.emergence
|
| 969 |
+
vineMaterial.uniforms.growthProgress.value = vineData.growthProgress
|
| 970 |
+
|
| 971 |
+
// Only update vine growth periodically for performance
|
| 972 |
+
if (currentTime - dysonSystem.lastUpdate < 50) return // Update every 50ms
|
| 973 |
+
dysonSystem.lastUpdate = currentTime
|
| 974 |
+
|
| 975 |
+
// Update vine growth
|
| 976 |
+
vineData.growthProgress = Math.min(1.0, vineData.growthProgress + vineGrowthRate * deltaTime)
|
| 977 |
+
|
| 978 |
+
let vineIndex = 0
|
| 979 |
+
|
| 980 |
+
// Reset all positions
|
| 981 |
+
vinePositions.fill(0)
|
| 982 |
+
vineOpacities.fill(0)
|
| 983 |
+
|
| 984 |
+
// Update each vine
|
| 985 |
+
vineData.vines.forEach((vine: any, vIndex: number) => {
|
| 986 |
+
if (vineIndex >= dysonSystem.maxVines) return
|
| 987 |
+
|
| 988 |
+
// Grow vine segments based on audio energy and SEFA
|
| 989 |
+
const audioInfluence = (audioData.overallAmplitude || 0) * config.vines.audioReactivity
|
| 990 |
+
const growthRate = config.vines.growthSpeed * (1 + audioInfluence) * vineData.growthProgress
|
| 991 |
+
|
| 992 |
+
vine.growth += growthRate * deltaTime
|
| 993 |
+
vine.maturity = Math.min(1.0, vine.maturity + deltaTime / config.growth.maturityTime)
|
| 994 |
+
|
| 995 |
+
// Add new segments as vine grows
|
| 996 |
+
if (vine.growth > 1.0 && vine.segments.length < config.vines.maxSegmentsPerVine) {
|
| 997 |
+
vine.growth = 0
|
| 998 |
+
|
| 999 |
+
const lastSegment = vine.segments[vine.segments.length - 1]
|
| 1000 |
+
|
| 1001 |
+
// Calculate organic growth direction
|
| 1002 |
+
const centerForce = {
|
| 1003 |
+
x: -lastSegment.x * 0.1,
|
| 1004 |
+
y: -lastSegment.y * 0.1,
|
| 1005 |
+
z: -lastSegment.z * 0.1
|
| 1006 |
+
}
|
| 1007 |
+
|
| 1008 |
+
// Add curvature and organic variation
|
| 1009 |
+
vine.direction.x += (Math.random() - 0.5) * config.vines.organicVariation + centerForce.x
|
| 1010 |
+
vine.direction.y += (Math.random() - 0.5) * config.vines.organicVariation + centerForce.y
|
| 1011 |
+
vine.direction.z += (Math.random() - 0.5) * config.vines.organicVariation + centerForce.z
|
| 1012 |
+
|
| 1013 |
+
// Normalize direction
|
| 1014 |
+
const dirLength = Math.sqrt(vine.direction.x ** 2 + vine.direction.y ** 2 + vine.direction.z ** 2)
|
| 1015 |
+
if (dirLength > 0) {
|
| 1016 |
+
vine.direction.x /= dirLength
|
| 1017 |
+
vine.direction.y /= dirLength
|
| 1018 |
+
vine.direction.z /= dirLength
|
| 1019 |
+
}
|
| 1020 |
+
|
| 1021 |
+
// Create new segment
|
| 1022 |
+
const segmentLength = 0.1 + sefaMetrics.emergence * 0.05
|
| 1023 |
+
const newSegment = {
|
| 1024 |
+
x: lastSegment.x + vine.direction.x * segmentLength,
|
| 1025 |
+
y: lastSegment.y + vine.direction.y * segmentLength,
|
| 1026 |
+
z: lastSegment.z + vine.direction.z * segmentLength
|
| 1027 |
+
}
|
| 1028 |
+
|
| 1029 |
+
vine.segments.push(newSegment)
|
| 1030 |
+
|
| 1031 |
+
// Update node positions
|
| 1032 |
+
if (dysonSystem.nodeInstances[vIndex]) {
|
| 1033 |
+
dysonSystem.nodeInstances[vIndex].position.set(newSegment.x, newSegment.y, newSegment.z)
|
| 1034 |
+
|
| 1035 |
+
// Update node material based on audio
|
| 1036 |
+
const nodeMat = dysonSystem.nodeInstances[vIndex].material as any
|
| 1037 |
+
nodeMat.opacity = 0.5 + 0.5 * audioData.overallAmplitude
|
| 1038 |
+
nodeMat.color.setHex(parseInt(colors.nodeColor.replace('#', ''), 16))
|
| 1039 |
+
}
|
| 1040 |
+
}
|
| 1041 |
+
|
| 1042 |
+
// Render vine segments
|
| 1043 |
+
for (let i = 0; i < vine.segments.length - 1 && vineIndex < dysonSystem.maxVines; i++) {
|
| 1044 |
+
const segment1 = vine.segments[i]
|
| 1045 |
+
const segment2 = vine.segments[i + 1]
|
| 1046 |
+
|
| 1047 |
+
const baseIndex = vineIndex * 6
|
| 1048 |
+
|
| 1049 |
+
// Set positions for line segment
|
| 1050 |
+
vinePositions[baseIndex] = segment1.x
|
| 1051 |
+
vinePositions[baseIndex + 1] = segment1.y
|
| 1052 |
+
vinePositions[baseIndex + 2] = segment1.z
|
| 1053 |
+
vinePositions[baseIndex + 3] = segment2.x
|
| 1054 |
+
vinePositions[baseIndex + 4] = segment2.y
|
| 1055 |
+
vinePositions[baseIndex + 5] = segment2.z
|
| 1056 |
+
|
| 1057 |
+
// Set colors based on maturity and audio
|
| 1058 |
+
const maturityColor = vine.maturity
|
| 1059 |
+
const audioColor = audioData.overallAmplitude || 0
|
| 1060 |
+
|
| 1061 |
+
const primaryColor = hexToRgb(colors.primaryVine)
|
| 1062 |
+
const secondaryColor = hexToRgb(colors.secondaryVine)
|
| 1063 |
+
const highlightColor = hexToRgb(colors.vineHighlight)
|
| 1064 |
+
|
| 1065 |
+
if (primaryColor && secondaryColor && highlightColor) {
|
| 1066 |
+
const blendedColor = {
|
| 1067 |
+
r: primaryColor.r * (1 - maturityColor) + secondaryColor.r * maturityColor,
|
| 1068 |
+
g: primaryColor.g * (1 - maturityColor) + secondaryColor.g * maturityColor,
|
| 1069 |
+
b: primaryColor.b * (1 - maturityColor) + secondaryColor.b * maturityColor
|
| 1070 |
+
}
|
| 1071 |
+
|
| 1072 |
+
// Apply audio highlight
|
| 1073 |
+
blendedColor.r = Math.min(1, blendedColor.r + highlightColor.r * audioColor * 0.3)
|
| 1074 |
+
blendedColor.g = Math.min(1, blendedColor.g + highlightColor.g * audioColor * 0.3)
|
| 1075 |
+
blendedColor.b = Math.min(1, blendedColor.b + highlightColor.b * audioColor * 0.3)
|
| 1076 |
+
|
| 1077 |
+
vineColors[baseIndex] = blendedColor.r
|
| 1078 |
+
vineColors[baseIndex + 1] = blendedColor.g
|
| 1079 |
+
vineColors[baseIndex + 2] = blendedColor.b
|
| 1080 |
+
vineColors[baseIndex + 3] = blendedColor.r
|
| 1081 |
+
vineColors[baseIndex + 4] = blendedColor.g
|
| 1082 |
+
vineColors[baseIndex + 5] = blendedColor.b
|
| 1083 |
+
}
|
| 1084 |
+
|
| 1085 |
+
// Set opacity based on growth and audio
|
| 1086 |
+
const opacity = vine.maturity * (0.6 + 0.4 * audioData.overallAmplitude)
|
| 1087 |
+
const opacityIndex = vineIndex * 2
|
| 1088 |
+
vineOpacities[opacityIndex] = opacity
|
| 1089 |
+
vineOpacities[opacityIndex + 1] = opacity
|
| 1090 |
+
|
| 1091 |
+
vineIndex++
|
| 1092 |
+
}
|
| 1093 |
+
})
|
| 1094 |
+
|
| 1095 |
+
// Mark geometry for update
|
| 1096 |
+
dysonSystem.vineGeometry.attributes.position.needsUpdate = true
|
| 1097 |
+
dysonSystem.vineGeometry.attributes.color.needsUpdate = true
|
| 1098 |
+
dysonSystem.vineGeometry.attributes.alpha.needsUpdate = true
|
| 1099 |
+
}
|
| 1100 |
+
|
| 1101 |
+
// Helper function to convert hex color to RGB
|
| 1102 |
+
const hexToRgb = (hex: string) => {
|
| 1103 |
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
| 1104 |
+
return result ? {
|
| 1105 |
+
r: parseInt(result[1], 16) / 255,
|
| 1106 |
+
g: parseInt(result[2], 16) / 255,
|
| 1107 |
+
b: parseInt(result[3], 16) / 255
|
| 1108 |
+
} : null
|
| 1109 |
+
}
|
| 1110 |
+
|
| 1111 |
+
// Helper function to create curved tendril points
|
| 1112 |
+
const getCurvedTendrilPoint = (x1: number, y1: number, z1: number, x2: number, y2: number, z2: number, t: number, time: number, sefaStrength: number) => {
|
| 1113 |
+
// Linear interpolation
|
| 1114 |
+
const x = x1 + (x2 - x1) * t
|
| 1115 |
+
const y = y1 + (y2 - y1) * t
|
| 1116 |
+
const z = z1 + (z2 - z1) * t
|
| 1117 |
+
|
| 1118 |
+
// Add curvature and SEFA-driven undulation
|
| 1119 |
+
const midPoint = t * (1 - t) * 4 // Peaks at t=0.5
|
| 1120 |
+
const curve = midPoint * 0.2 * sefaStrength
|
| 1121 |
+
const undulation = 0.05 * sefaStrength * Math.sin(time * 0.002 + t * 10)
|
| 1122 |
+
|
| 1123 |
+
return {
|
| 1124 |
+
x: x + curve * Math.sin(time * 0.001 + t * 5) + undulation,
|
| 1125 |
+
y: y + curve * Math.cos(time * 0.001 + t * 5) + undulation * 0.5,
|
| 1126 |
+
z: z + curve * Math.sin(time * 0.0015 + t * 3) + undulation * 0.3
|
| 1127 |
+
}
|
| 1128 |
+
}
|
| 1129 |
+
|
| 1130 |
+
// Helper function to get frequency-based color with organic palette
|
| 1131 |
+
const getFrequencyColor = (frequency: number, intensity: number) => {
|
| 1132 |
+
const config = configLoader.getConfig()
|
| 1133 |
+
const organicBlend = (config as any)?.colors?.tendrilColors?.organicBlend
|
| 1134 |
+
|
| 1135 |
+
if (organicBlend) {
|
| 1136 |
+
const normalizedFreq = Math.min(1, frequency / 10000)
|
| 1137 |
+
|
| 1138 |
+
// Use organic color blending
|
| 1139 |
+
let color
|
| 1140 |
+
if (normalizedFreq < 0.2) {
|
| 1141 |
+
color = organicBlend.forest // Deep greens for low frequencies
|
| 1142 |
+
} else if (normalizedFreq < 0.4) {
|
| 1143 |
+
color = organicBlend.bronze // Bronze for low-mid frequencies
|
| 1144 |
+
} else if (normalizedFreq < 0.6) {
|
| 1145 |
+
color = organicBlend.copper // Copper for mid frequencies
|
| 1146 |
+
} else if (normalizedFreq < 0.8) {
|
| 1147 |
+
color = organicBlend.gold // Gold for high-mid frequencies
|
| 1148 |
+
} else {
|
| 1149 |
+
color = organicBlend.teal // Teal for high frequencies
|
| 1150 |
+
}
|
| 1151 |
+
|
| 1152 |
+
return {
|
| 1153 |
+
r: color[0] * intensity,
|
| 1154 |
+
g: color[1] * intensity,
|
| 1155 |
+
b: color[2] * intensity
|
| 1156 |
+
}
|
| 1157 |
+
}
|
| 1158 |
+
|
| 1159 |
+
// Fallback to original calculation
|
| 1160 |
+
const normalizedFreq = Math.min(1, frequency / 10000)
|
| 1161 |
+
return {
|
| 1162 |
+
r: (0.8 + 0.2 * normalizedFreq) * intensity,
|
| 1163 |
+
g: (0.6 + 0.4 * Math.sin(normalizedFreq * Math.PI)) * intensity,
|
| 1164 |
+
b: (0.2 + 0.8 * (1 - normalizedFreq)) * intensity
|
| 1165 |
+
}
|
| 1166 |
+
}
|
| 1167 |
+
|
| 1168 |
+
// Organic cymatic pattern generator - creates natural, sound-driven formations
|
| 1169 |
+
const getCymatePattern = useCallback((x: number, y: number, z: number, frequency: number, time: number) => {
|
| 1170 |
+
// Create organic patterns inspired by natural growth and cymatics
|
| 1171 |
+
const r = Math.sqrt(x*x + y*y + z*z)
|
| 1172 |
+
const theta = Math.atan2(y, x)
|
| 1173 |
+
const phi = Math.acos(z / (r || 1))
|
| 1174 |
+
|
| 1175 |
+
// Frequency-based pattern selection with organic variation
|
| 1176 |
+
const freqFactor = frequency / 1000
|
| 1177 |
+
const pattern = Math.floor(freqFactor) % 5 // Added 5th pattern
|
| 1178 |
+
|
| 1179 |
+
let patternX = 0, patternY = 0, patternZ = 0
|
| 1180 |
+
|
| 1181 |
+
// Load config for cymatic pattern settings
|
| 1182 |
+
const config = configLoader.getConfig()
|
| 1183 |
+
const cymateConfig = (config as any)?.cymatics?.patterns
|
| 1184 |
+
|
| 1185 |
+
switch (pattern) {
|
| 1186 |
+
case 0: // Organic radial waves (tree rings, water ripples)
|
| 1187 |
+
const radialFreq = cymateConfig?.radialWaves?.frequencyFactor || 0.08
|
| 1188 |
+
const radialPetals = cymateConfig?.radialWaves?.petalCount || 6
|
| 1189 |
+
const radialPattern = Math.sin(r * radialFreq + time * 0.8) * Math.cos(theta * radialPetals + time * 0.3)
|
| 1190 |
+
patternX = radialPattern * Math.cos(theta) * 0.7
|
| 1191 |
+
patternY = radialPattern * Math.sin(theta) * 0.7
|
| 1192 |
+
patternZ = Math.sin(phi * 3 + time * 0.4) * (cymateConfig?.radialWaves?.amplitudeZ || 0.4)
|
| 1193 |
+
break
|
| 1194 |
+
|
| 1195 |
+
case 1: // Natural spiral harmonics (nautilus, plant growth)
|
| 1196 |
+
const l = Math.max(1, Math.floor(freqFactor * 0.4) + 1)
|
| 1197 |
+
const m = Math.floor(freqFactor * 0.25)
|
| 1198 |
+
const spiralFactor = 1 + 0.3 * Math.sin(time * 0.6)
|
| 1199 |
+
patternX = Math.sin(l * phi * spiralFactor) * Math.cos(m * theta + time * 0.7)
|
| 1200 |
+
patternY = Math.sin(l * phi * spiralFactor) * Math.sin(m * theta + time * 0.7)
|
| 1201 |
+
patternZ = Math.cos(l * phi) * Math.sin(time * 1.2) * 0.6
|
| 1202 |
+
break
|
| 1203 |
+
|
| 1204 |
+
case 2: // Organic lattice (honeycomb, crystal growth)
|
| 1205 |
+
const latticeFreq = cymateConfig?.geometricLattice?.frequencyFactor || 0.15
|
| 1206 |
+
const organic = 0.8 + 0.2 * Math.sin(time * 0.5)
|
| 1207 |
+
patternX = (Math.sin(x * latticeFreq + time * 0.6) + Math.sin(y * latticeFreq * 1.1)) * organic
|
| 1208 |
+
patternY = (Math.sin(y * latticeFreq + time * 0.8) + Math.sin(z * latticeFreq * 0.9)) * organic
|
| 1209 |
+
patternZ = (Math.sin(z * latticeFreq + time * 0.7) + Math.sin(x * latticeFreq * 1.05)) * organic
|
| 1210 |
+
break
|
| 1211 |
+
|
| 1212 |
+
case 3: // Enhanced flower of life (sacred geometry)
|
| 1213 |
+
const flowerFreq = cymateConfig?.flowerOfLife?.frequencyFactor || 0.12
|
| 1214 |
+
const basePetals = cymateConfig?.flowerOfLife?.basePetals || 6
|
| 1215 |
+
const petalVariation = cymateConfig?.flowerOfLife?.petalVariation || 0.1
|
| 1216 |
+
const flowerPetals = basePetals + Math.floor(freqFactor * petalVariation * 10)
|
| 1217 |
+
const bloom = 0.7 + 0.3 * Math.sin(time * 0.4)
|
| 1218 |
+
patternX = Math.sin(theta * flowerPetals + time * 0.5) * Math.sin(r * flowerFreq) * bloom
|
| 1219 |
+
patternY = Math.cos(theta * flowerPetals + time * 0.5) * Math.sin(r * flowerFreq) * bloom
|
| 1220 |
+
patternZ = Math.sin(phi * flowerPetals * 0.7 + time * 0.6) * (cymateConfig?.flowerOfLife?.amplitudeZ || 0.25)
|
| 1221 |
+
break
|
| 1222 |
+
|
| 1223 |
+
case 4: // New: Organic vine growth pattern
|
| 1224 |
+
const vineFreq = freqFactor * 0.1
|
| 1225 |
+
const vineSpiral = theta + r * 0.5 + time * 0.3
|
| 1226 |
+
const growth = 0.6 + 0.4 * Math.sin(time * 0.35)
|
| 1227 |
+
patternX = Math.sin(vineSpiral) * Math.exp(-r * 0.2) * growth
|
| 1228 |
+
patternY = Math.cos(vineSpiral) * Math.exp(-r * 0.2) * growth
|
| 1229 |
+
patternZ = Math.sin(r * vineFreq + time * 0.5) * 0.4 * growth
|
| 1230 |
+
break
|
| 1231 |
+
}
|
| 1232 |
+
|
| 1233 |
+
// Organic breathing effect with natural variation
|
| 1234 |
+
const breathing = cymateConfig?.breathing
|
| 1235 |
+
const breathingSpeed = breathing?.speed || 0.4
|
| 1236 |
+
const intensityBase = breathing?.intensityBase || 0.25
|
| 1237 |
+
const intensityVariation = breathing?.intensityVariation || 0.6
|
| 1238 |
+
|
| 1239 |
+
const intensity = intensityBase + intensityVariation * Math.sin(time * breathingSpeed) *
|
| 1240 |
+
(0.8 + 0.2 * Math.sin(time * breathingSpeed * 0.7)) // Natural variation
|
| 1241 |
+
|
| 1242 |
+
return {
|
| 1243 |
+
x: patternX * intensity,
|
| 1244 |
+
y: patternY * intensity,
|
| 1245 |
+
z: patternZ * intensity
|
| 1246 |
+
}
|
| 1247 |
+
}, [cymateGeometry])
|
| 1248 |
+
|
| 1249 |
+
// Update individual sphere particles with performance optimization
|
| 1250 |
+
const updateSphereParticles = (
|
| 1251 |
+
sphere: ParticleSphere,
|
| 1252 |
+
currentTime: number,
|
| 1253 |
+
deltaTime: number,
|
| 1254 |
+
beatDetected: boolean
|
| 1255 |
+
) => {
|
| 1256 |
+
const { params, positions, velocities, lifetimes, beatEffects } = sphere
|
| 1257 |
+
const noise = noiseGeneratorRef.current!
|
| 1258 |
+
const beatManager = beatManagerRef.current!
|
| 1259 |
+
|
| 1260 |
+
// Performance optimization: Skip heavy calculations if needed
|
| 1261 |
+
const adaptiveLevel = 1.0 // Simplified - no performance monitoring
|
| 1262 |
+
|
| 1263 |
+
// Frame skipping for low performance - simplified
|
| 1264 |
+
|
| 1265 |
+
// STABILITY: Clamp deltaTime to prevent integration instability
|
| 1266 |
+
const clampedDeltaTime = Math.min(deltaTime, 1/30) // Max 30 FPS equivalent timestep
|
| 1267 |
+
const config = configLoader.getConfig()
|
| 1268 |
+
const maxVelocity = config?.visualization?.physics?.maxVelocity || 0.1
|
| 1269 |
+
const stabilityThreshold = config?.visualization?.physics?.stabilityThreshold || 10.0
|
| 1270 |
+
|
| 1271 |
+
// Adaptive particle count based on performance
|
| 1272 |
+
const adaptiveParticleCount = Math.min(params.particleCount,
|
| 1273 |
+
adaptiveLevel * 15000) // Simplified - no dynamic particle count
|
| 1274 |
+
|
| 1275 |
+
// Level of detail calculation
|
| 1276 |
+
const particleUpdateStep = Math.max(1, Math.floor(1 / adaptiveLevel))
|
| 1277 |
+
|
| 1278 |
+
for (let i = 0; i < adaptiveParticleCount; i += particleUpdateStep) {
|
| 1279 |
+
const i3 = i * 3
|
| 1280 |
+
|
| 1281 |
+
let x = positions[i3]
|
| 1282 |
+
let y = positions[i3 + 1]
|
| 1283 |
+
let z = positions[i3 + 2]
|
| 1284 |
+
|
| 1285 |
+
let vx = velocities[i3]
|
| 1286 |
+
let vy = velocities[i3 + 1]
|
| 1287 |
+
let vz = velocities[i3 + 2]
|
| 1288 |
+
|
| 1289 |
+
let lt = lifetimes[i]
|
| 1290 |
+
let be = beatEffects[i]
|
| 1291 |
+
|
| 1292 |
+
// STABILITY: Check for NaN or infinite values
|
| 1293 |
+
if (!isFinite(x) || !isFinite(y) || !isFinite(z) ||
|
| 1294 |
+
!isFinite(vx) || !isFinite(vy) || !isFinite(vz)) {
|
| 1295 |
+
// Reset particle to safe position
|
| 1296 |
+
const theta = Math.random() * Math.PI * 2
|
| 1297 |
+
const phi = Math.acos(2 * Math.random() - 1)
|
| 1298 |
+
const r = Math.cbrt(Math.random()) * params.sphereRadius * 0.5
|
| 1299 |
+
|
| 1300 |
+
x = r * Math.sin(phi) * Math.cos(theta)
|
| 1301 |
+
y = r * Math.sin(phi) * Math.sin(theta)
|
| 1302 |
+
z = r * Math.cos(phi)
|
| 1303 |
+
|
| 1304 |
+
vx = vy = vz = 0
|
| 1305 |
+
console.warn(`🚨 Particle ${i} reset due to invalid values`)
|
| 1306 |
+
}
|
| 1307 |
+
|
| 1308 |
+
// STABILITY: Check distance from origin - emergency containment
|
| 1309 |
+
const distFromOrigin = Math.sqrt(x*x + y*y + z*z)
|
| 1310 |
+
if (distFromOrigin > stabilityThreshold) {
|
| 1311 |
+
// Pull particle back to safe zone
|
| 1312 |
+
const pullFactor = 0.1
|
| 1313 |
+
x *= pullFactor
|
| 1314 |
+
y *= pullFactor
|
| 1315 |
+
z *= pullFactor
|
| 1316 |
+
vx *= 0.1 // Severe velocity damping
|
| 1317 |
+
vy *= 0.1
|
| 1318 |
+
vz *= 0.1
|
| 1319 |
+
console.warn(`🚨 Particle ${i} emergency reset - distance: ${distFromOrigin.toFixed(2)}`)
|
| 1320 |
+
}
|
| 1321 |
+
|
| 1322 |
+
// Update lifetime
|
| 1323 |
+
lt -= clampedDeltaTime
|
| 1324 |
+
|
| 1325 |
+
// Apply noise-based turbulence with caching
|
| 1326 |
+
const ns = params.noiseScale * adaptiveLevel // Scale noise with performance
|
| 1327 |
+
const speed = params.noiseSpeed
|
| 1328 |
+
const timeFactor = currentTime * 0.001
|
| 1329 |
+
|
| 1330 |
+
// Calculate noise values directly - no caching overhead
|
| 1331 |
+
const noiseValues = {
|
| 1332 |
+
x: noise.noise3D(x * ns + timeFactor * speed, y * ns, z * ns),
|
| 1333 |
+
y: noise.noise3D(x * ns, y * ns + timeFactor * speed, z * ns),
|
| 1334 |
+
z: noise.noise3D(x * ns, y * ns, z * ns + timeFactor * speed)
|
| 1335 |
+
}
|
| 1336 |
+
|
| 1337 |
+
const noiseX = noiseValues.x
|
| 1338 |
+
const noiseY = noiseValues.y
|
| 1339 |
+
const noiseZ = noiseValues.z
|
| 1340 |
+
|
| 1341 |
+
// STABILITY: Scale turbulence by performance and distance
|
| 1342 |
+
const performanceScale = Math.min(1.0, 60 * clampedDeltaTime) // Scale down for low FPS
|
| 1343 |
+
const turbulenceScale = params.turbulenceStrength * performanceScale
|
| 1344 |
+
|
| 1345 |
+
vx += noiseX * turbulenceScale
|
| 1346 |
+
vy += noiseY * turbulenceScale
|
| 1347 |
+
vz += noiseZ * turbulenceScale
|
| 1348 |
+
|
| 1349 |
+
// Apply beat effects
|
| 1350 |
+
if (beatDetected) {
|
| 1351 |
+
be = 1.0
|
| 1352 |
+
}
|
| 1353 |
+
be *= 0.95
|
| 1354 |
+
|
| 1355 |
+
if (be > 0.01) {
|
| 1356 |
+
const dist = Math.sqrt(x*x + y*y + z*z)
|
| 1357 |
+
if (dist > 0) {
|
| 1358 |
+
const dx = x / dist
|
| 1359 |
+
const dy = y / dist
|
| 1360 |
+
const dz = z / dist
|
| 1361 |
+
|
| 1362 |
+
const beatForce = be * params.beatStrength
|
| 1363 |
+
vx += dx * beatForce
|
| 1364 |
+
vy += dy * beatForce
|
| 1365 |
+
vz += dz * beatForce
|
| 1366 |
+
}
|
| 1367 |
+
}
|
| 1368 |
+
|
| 1369 |
+
// Apply wave forces
|
| 1370 |
+
const waveForce = beatManager.getWaveForce({ x, y, z })
|
| 1371 |
+
if (waveForce > 0) {
|
| 1372 |
+
const dist = Math.sqrt(x*x + y*y + z*z)
|
| 1373 |
+
if (dist > 0) {
|
| 1374 |
+
const dx = x / dist
|
| 1375 |
+
const dy = y / dist
|
| 1376 |
+
const dz = z / dist
|
| 1377 |
+
|
| 1378 |
+
vx += dx * waveForce * 0.001
|
| 1379 |
+
vy += dy * waveForce * 0.001
|
| 1380 |
+
vz += dz * waveForce * 0.001
|
| 1381 |
+
}
|
| 1382 |
+
}
|
| 1383 |
+
|
| 1384 |
+
// Cymatic geometry patterns - sound-driven geometric formations with organic influence
|
| 1385 |
+
if (cymateGeometry && audioEngine) {
|
| 1386 |
+
const freq = sphere.params.minFrequency +
|
| 1387 |
+
(sphere.params.maxFrequency - sphere.params.minFrequency) * 0.5
|
| 1388 |
+
|
| 1389 |
+
// Create geometric patterns based on frequency
|
| 1390 |
+
const geometricPattern = getCymatePattern(x, y, z, freq, currentTime * 0.001)
|
| 1391 |
+
|
| 1392 |
+
// Reduce intensity for more organic, subtle movement
|
| 1393 |
+
const organicIntensity = 0.0015 * (0.8 + 0.2 * Math.sin(currentTime * 0.0005))
|
| 1394 |
+
vx += geometricPattern.x * organicIntensity
|
| 1395 |
+
vy += geometricPattern.y * organicIntensity
|
| 1396 |
+
vz += geometricPattern.z * organicIntensity
|
| 1397 |
+
}
|
| 1398 |
+
|
| 1399 |
+
// Update positions
|
| 1400 |
+
x += vx
|
| 1401 |
+
y += vy
|
| 1402 |
+
z += vz
|
| 1403 |
+
|
| 1404 |
+
// ENTRONAUT: Apply enhanced biomimetic coupling parameters
|
| 1405 |
+
const entronautParams = applyEntronautCoupling(sphere, i)
|
| 1406 |
+
|
| 1407 |
+
// Optimized living entity behavior: adaptive computational load
|
| 1408 |
+
const entronautStep = Math.max(10, Math.floor(30 / adaptiveLevel)) // More aggressive stepping at low performance
|
| 1409 |
+
if (entronautEnabled && adaptiveCoupling && (i % entronautStep === 0)) { // Adaptive step size
|
| 1410 |
+
// Simplified flocking behavior with fewer neighbor checks
|
| 1411 |
+
let avgVx = 0, avgVy = 0, avgVz = 0
|
| 1412 |
+
let neighborCount = 0
|
| 1413 |
+
const flockRadius = 0.25
|
| 1414 |
+
|
| 1415 |
+
// Check only a few nearby particles for performance
|
| 1416 |
+
for (let j = Math.max(0, i - 20); j < Math.min(params.particleCount, i + 20); j += 5) {
|
| 1417 |
+
if (j === i) continue
|
| 1418 |
+
const j3 = j * 3
|
| 1419 |
+
const dx = positions[j3] - x
|
| 1420 |
+
const dy = positions[j3 + 1] - y
|
| 1421 |
+
const dz = positions[j3 + 2] - z
|
| 1422 |
+
const distSq = dx*dx + dy*dy + dz*dz // Use squared distance to avoid sqrt
|
| 1423 |
+
|
| 1424 |
+
if (distSq < flockRadius * flockRadius && distSq > 0) {
|
| 1425 |
+
avgVx += velocities[j3]
|
| 1426 |
+
avgVy += velocities[j3 + 1]
|
| 1427 |
+
avgVz += velocities[j3 + 2]
|
| 1428 |
+
neighborCount++
|
| 1429 |
+
}
|
| 1430 |
+
}
|
| 1431 |
+
|
| 1432 |
+
if (neighborCount > 0) {
|
| 1433 |
+
avgVx /= neighborCount
|
| 1434 |
+
avgVy /= neighborCount
|
| 1435 |
+
avgVz /= neighborCount
|
| 1436 |
+
|
| 1437 |
+
// Apply simplified collective behavior
|
| 1438 |
+
const flockInfluence = entronautParams.coupling * 0.2
|
| 1439 |
+
vx += (avgVx - vx) * flockInfluence
|
| 1440 |
+
vy += (avgVy - vy) * flockInfluence
|
| 1441 |
+
vz += (avgVz - vz) * flockInfluence
|
| 1442 |
+
}
|
| 1443 |
+
}
|
| 1444 |
+
|
| 1445 |
+
// Simplified metabolic pulsing for all particles
|
| 1446 |
+
if (entronautEnabled && adaptiveCoupling) {
|
| 1447 |
+
const metabolicPulse = entronautParams.diffusion * 0.5
|
| 1448 |
+
vx += metabolicPulse * Math.cos(i * 0.01)
|
| 1449 |
+
vy += metabolicPulse * Math.sin(i * 0.01)
|
| 1450 |
+
}
|
| 1451 |
+
|
| 1452 |
+
// STABILITY: Velocity clamping to prevent runaway particles
|
| 1453 |
+
const velocity = Math.sqrt(vx*vx + vy*vy + vz*vz)
|
| 1454 |
+
if (velocity > maxVelocity) {
|
| 1455 |
+
const scale = maxVelocity / velocity
|
| 1456 |
+
vx *= scale
|
| 1457 |
+
vy *= scale
|
| 1458 |
+
vz *= scale
|
| 1459 |
+
}
|
| 1460 |
+
|
| 1461 |
+
// Apply adaptive drag/damping with living characteristics
|
| 1462 |
+
const dampingFactor = entronautParams.damping
|
| 1463 |
+
vx *= dampingFactor
|
| 1464 |
+
vy *= dampingFactor
|
| 1465 |
+
vz *= dampingFactor
|
| 1466 |
+
|
| 1467 |
+
// Enhanced containment system with progressive damping
|
| 1468 |
+
const dist = Math.sqrt(x*x + y*y + z*z)
|
| 1469 |
+
const sphereRadius = params.sphereRadius
|
| 1470 |
+
|
| 1471 |
+
if (dist > sphereRadius) {
|
| 1472 |
+
const overflow = dist - sphereRadius
|
| 1473 |
+
const pullback = overflow * (config?.visualization?.physics?.emergencyDamping || 0.1)
|
| 1474 |
+
|
| 1475 |
+
if (dist > 0) {
|
| 1476 |
+
const dx = x / dist
|
| 1477 |
+
const dy = y / dist
|
| 1478 |
+
const dz = z / dist
|
| 1479 |
+
|
| 1480 |
+
// Progressive pullback force
|
| 1481 |
+
x -= dx * pullback
|
| 1482 |
+
y -= dy * pullback
|
| 1483 |
+
z -= dz * pullback
|
| 1484 |
+
|
| 1485 |
+
// Stronger damping for particles outside bounds
|
| 1486 |
+
const dampingStrength = Math.min(0.5, overflow / sphereRadius)
|
| 1487 |
+
vx *= (1 - dampingStrength)
|
| 1488 |
+
vy *= (1 - dampingStrength)
|
| 1489 |
+
vz *= (1 - dampingStrength)
|
| 1490 |
+
}
|
| 1491 |
+
}
|
| 1492 |
+
|
| 1493 |
+
// Soft containment - add inward force near boundaries
|
| 1494 |
+
const boundaryZone = sphereRadius * 0.8
|
| 1495 |
+
if (dist > boundaryZone && dist > 0) {
|
| 1496 |
+
const boundaryForce = (dist - boundaryZone) / (sphereRadius - boundaryZone) * 0.001
|
| 1497 |
+
const dx = x / dist
|
| 1498 |
+
const dy = y / dist
|
| 1499 |
+
const dz = z / dist
|
| 1500 |
+
|
| 1501 |
+
vx -= dx * boundaryForce
|
| 1502 |
+
vy -= dy * boundaryForce
|
| 1503 |
+
vz -= dz * boundaryForce
|
| 1504 |
+
}
|
| 1505 |
+
|
| 1506 |
+
// Respawn dead particles
|
| 1507 |
+
if (lt <= 0) {
|
| 1508 |
+
const radius = params.sphereRadius * params.innerSphereRadius
|
| 1509 |
+
const theta = Math.random() * Math.PI * 2
|
| 1510 |
+
const phi = Math.acos(2 * Math.random() - 1)
|
| 1511 |
+
const r = Math.cbrt(Math.random()) * radius
|
| 1512 |
+
|
| 1513 |
+
x = r * Math.sin(phi) * Math.cos(theta)
|
| 1514 |
+
y = r * Math.sin(phi) * Math.sin(theta)
|
| 1515 |
+
z = r * Math.cos(phi)
|
| 1516 |
+
|
| 1517 |
+
vx = 0
|
| 1518 |
+
vy = 0
|
| 1519 |
+
vz = 0
|
| 1520 |
+
|
| 1521 |
+
lt = Math.random() * params.particleLifetime
|
| 1522 |
+
be = 0
|
| 1523 |
+
}
|
| 1524 |
+
|
| 1525 |
+
// Update arrays
|
| 1526 |
+
positions[i3] = x
|
| 1527 |
+
positions[i3 + 1] = y
|
| 1528 |
+
positions[i3 + 2] = z
|
| 1529 |
+
|
| 1530 |
+
velocities[i3] = vx
|
| 1531 |
+
velocities[i3 + 1] = vy
|
| 1532 |
+
velocities[i3 + 2] = vz
|
| 1533 |
+
|
| 1534 |
+
lifetimes[i] = lt
|
| 1535 |
+
beatEffects[i] = be
|
| 1536 |
+
}
|
| 1537 |
+
|
| 1538 |
+
// Mark geometry for update
|
| 1539 |
+
if (sceneRef.current) {
|
| 1540 |
+
const particleSystem = sceneRef.current.children.find((child: any) =>
|
| 1541 |
+
child.userData?.sphereIndex === sphere.index
|
| 1542 |
+
) as any
|
| 1543 |
+
if (particleSystem?.geometry) {
|
| 1544 |
+
particleSystem.geometry.attributes.position.needsUpdate = true
|
| 1545 |
+
}
|
| 1546 |
+
}
|
| 1547 |
+
}
|
| 1548 |
+
|
| 1549 |
+
// Update sphere rotation based on audio
|
| 1550 |
+
const updateSphereRotation = (sphere: ParticleSphere, _audioData: any) => {
|
| 1551 |
+
if (!audioEngine) return
|
| 1552 |
+
|
| 1553 |
+
const volumeData = audioEngine.getSmoothVolume(
|
| 1554 |
+
sphere.lastValidVolume,
|
| 1555 |
+
sphere.params.volumeChangeThreshold
|
| 1556 |
+
)
|
| 1557 |
+
|
| 1558 |
+
if (volumeData.shouldUpdate) {
|
| 1559 |
+
const targetRotationSpeed = sphere.params.rotationSpeedMin +
|
| 1560 |
+
(sphere.params.rotationSpeedMax - sphere.params.rotationSpeedMin) * volumeData.volume
|
| 1561 |
+
|
| 1562 |
+
sphere.lastRotationSpeed = sphere.params.rotationSpeed +
|
| 1563 |
+
(targetRotationSpeed - sphere.params.rotationSpeed) * sphere.params.rotationSmoothness
|
| 1564 |
+
|
| 1565 |
+
sphere.lastValidVolume = volumeData.volume
|
| 1566 |
+
}
|
| 1567 |
+
|
| 1568 |
+
// Apply rotation
|
| 1569 |
+
if (sceneRef.current) {
|
| 1570 |
+
const particleSystem = sceneRef.current.children.find((child: any) =>
|
| 1571 |
+
child.userData?.sphereIndex === sphere.index
|
| 1572 |
+
) as any
|
| 1573 |
+
if (particleSystem) {
|
| 1574 |
+
particleSystem.rotation.y += sphere.lastRotationSpeed
|
| 1575 |
+
}
|
| 1576 |
+
}
|
| 1577 |
+
}
|
| 1578 |
+
|
| 1579 |
+
// Camera control functions
|
| 1580 |
+
const resetCameraPosition = useCallback(() => {
|
| 1581 |
+
if (!cameraRef.current || !controlsRef.current) return
|
| 1582 |
+
|
| 1583 |
+
cameraRef.current.position.set(0, 0, 2.5)
|
| 1584 |
+
controlsRef.current.target.set(0, 0, 0)
|
| 1585 |
+
controlsRef.current.update()
|
| 1586 |
+
}, [])
|
| 1587 |
+
|
| 1588 |
+
const setCameraPreset = useCallback((preset: 'default' | 'inside' | 'far' | 'top' | 'side') => {
|
| 1589 |
+
if (!cameraRef.current || !controlsRef.current) return
|
| 1590 |
+
|
| 1591 |
+
switch (preset) {
|
| 1592 |
+
case 'default':
|
| 1593 |
+
cameraRef.current.position.set(0, 0, 2.5)
|
| 1594 |
+
break
|
| 1595 |
+
case 'inside':
|
| 1596 |
+
cameraRef.current.position.set(0, 0, 0.5)
|
| 1597 |
+
break
|
| 1598 |
+
case 'far':
|
| 1599 |
+
cameraRef.current.position.set(0, 0, 8)
|
| 1600 |
+
break
|
| 1601 |
+
case 'top':
|
| 1602 |
+
cameraRef.current.position.set(0, 5, 0)
|
| 1603 |
+
break
|
| 1604 |
+
case 'side':
|
| 1605 |
+
cameraRef.current.position.set(5, 0, 0)
|
| 1606 |
+
break
|
| 1607 |
+
}
|
| 1608 |
+
|
| 1609 |
+
controlsRef.current.target.set(0, 0, 0)
|
| 1610 |
+
controlsRef.current.update()
|
| 1611 |
+
}, [])
|
| 1612 |
+
|
| 1613 |
+
// Handle window resize
|
| 1614 |
+
const handleResize = useCallback(() => {
|
| 1615 |
+
if (!cameraRef.current || !rendererRef.current) return
|
| 1616 |
+
|
| 1617 |
+
cameraRef.current.aspect = window.innerWidth / window.innerHeight
|
| 1618 |
+
cameraRef.current.updateProjectionMatrix()
|
| 1619 |
+
rendererRef.current.setSize(window.innerWidth, window.innerHeight)
|
| 1620 |
+
|
| 1621 |
+
// Update controls on resize
|
| 1622 |
+
if (controlsRef.current) {
|
| 1623 |
+
controlsRef.current.handleResize?.()
|
| 1624 |
+
}
|
| 1625 |
+
}, [])
|
| 1626 |
+
|
| 1627 |
+
// Initialize on mount
|
| 1628 |
+
useEffect(() => {
|
| 1629 |
+
initializeVisualization()
|
| 1630 |
+
window.addEventListener('resize', handleResize)
|
| 1631 |
+
|
| 1632 |
+
// Listen for camera control disable events from audio dock
|
| 1633 |
+
const handleCameraControlToggle = (event: CustomEvent) => {
|
| 1634 |
+
if (controlsRef.current) {
|
| 1635 |
+
controlsRef.current.enabled = !event.detail && cameraControlsEnabled
|
| 1636 |
+
}
|
| 1637 |
+
}
|
| 1638 |
+
|
| 1639 |
+
window.addEventListener('disableCameraControls', handleCameraControlToggle as EventListener)
|
| 1640 |
+
|
| 1641 |
+
return () => {
|
| 1642 |
+
window.removeEventListener('resize', handleResize)
|
| 1643 |
+
window.removeEventListener('disableCameraControls', handleCameraControlToggle as EventListener)
|
| 1644 |
+
if (animationRef.current) {
|
| 1645 |
+
cancelAnimationFrame(animationRef.current)
|
| 1646 |
+
}
|
| 1647 |
+
if (controlsRef.current) {
|
| 1648 |
+
controlsRef.current.dispose()
|
| 1649 |
+
}
|
| 1650 |
+
}
|
| 1651 |
+
}, [initializeVisualization, handleResize, cameraControlsEnabled])
|
| 1652 |
+
|
| 1653 |
+
// Start animation when ready
|
| 1654 |
+
useEffect(() => {
|
| 1655 |
+
if (isVisualizationReady) {
|
| 1656 |
+
animationRef.current = requestAnimationFrame(animate)
|
| 1657 |
+
}
|
| 1658 |
+
return () => {
|
| 1659 |
+
if (animationRef.current) {
|
| 1660 |
+
cancelAnimationFrame(animationRef.current)
|
| 1661 |
+
}
|
| 1662 |
+
}
|
| 1663 |
+
}, [isVisualizationReady, animate])
|
| 1664 |
+
|
| 1665 |
+
return (
|
| 1666 |
+
<div className="world-tree-visualizer">
|
| 1667 |
+
<canvas
|
| 1668 |
+
ref={canvasRef}
|
| 1669 |
+
className="visualization-canvas"
|
| 1670 |
+
style={{
|
| 1671 |
+
position: 'absolute',
|
| 1672 |
+
top: 0,
|
| 1673 |
+
left: 0,
|
| 1674 |
+
width: '100%',
|
| 1675 |
+
height: '100%',
|
| 1676 |
+
background: '#0D0A07',
|
| 1677 |
+
pointerEvents: 'auto'
|
| 1678 |
+
}}
|
| 1679 |
+
onMouseEnter={() => {
|
| 1680 |
+
// Enable camera controls when mouse enters visualization area
|
| 1681 |
+
if (controlsRef.current && cameraControlsEnabled) {
|
| 1682 |
+
controlsRef.current.enabled = true
|
| 1683 |
+
}
|
| 1684 |
+
}}
|
| 1685 |
+
/>
|
| 1686 |
+
|
| 1687 |
+
{/* Compact Controls Panel */}
|
| 1688 |
+
<div
|
| 1689 |
+
className="particle-controls ancient-panel"
|
| 1690 |
+
style={{
|
| 1691 |
+
position: 'fixed',
|
| 1692 |
+
bottom: '20px',
|
| 1693 |
+
right: '20px',
|
| 1694 |
+
zIndex: 1000,
|
| 1695 |
+
background: 'linear-gradient(135deg, var(--marble-dark), var(--teal-dark))',
|
| 1696 |
+
borderRadius: '8px',
|
| 1697 |
+
color: 'var(--copper-light)',
|
| 1698 |
+
fontFamily: 'Metamorphous, serif',
|
| 1699 |
+
maxWidth: '280px',
|
| 1700 |
+
maxHeight: '75vh',
|
| 1701 |
+
transition: 'all 0.4s ease-in-out',
|
| 1702 |
+
transform: controlsCollapsed ? 'translateX(calc(100% - 55px))' : 'translateX(0)',
|
| 1703 |
+
backdropFilter: 'blur(15px)',
|
| 1704 |
+
border: '2px solid var(--copper-medium)',
|
| 1705 |
+
pointerEvents: 'auto',
|
| 1706 |
+
boxShadow: '0 0 20px var(--copper-dark), inset 0 0 10px var(--teal-dark)',
|
| 1707 |
+
display: 'flex',
|
| 1708 |
+
flexDirection: 'column'
|
| 1709 |
+
}}
|
| 1710 |
+
onMouseEnter={() => {
|
| 1711 |
+
// Disable camera controls when mouse enters controls panel
|
| 1712 |
+
const event = new CustomEvent('disableCameraControls', { detail: true })
|
| 1713 |
+
window.dispatchEvent(event)
|
| 1714 |
+
}}
|
| 1715 |
+
onMouseLeave={() => {
|
| 1716 |
+
// Re-enable camera controls when mouse leaves controls panel
|
| 1717 |
+
const event = new CustomEvent('disableCameraControls', { detail: false })
|
| 1718 |
+
window.dispatchEvent(event)
|
| 1719 |
+
}}
|
| 1720 |
+
>
|
| 1721 |
+
{/* Collapse/Expand Toggle */}
|
| 1722 |
+
<button
|
| 1723 |
+
onClick={() => setControlsCollapsed(!controlsCollapsed)}
|
| 1724 |
+
onMouseEnter={(e) => {
|
| 1725 |
+
e.currentTarget.style.transform = 'translateY(-50%) scale(1.1)'
|
| 1726 |
+
e.currentTarget.style.borderColor = 'var(--transmission-glow)'
|
| 1727 |
+
e.currentTarget.style.boxShadow = '0 0 15px var(--transmission-glow)'
|
| 1728 |
+
}}
|
| 1729 |
+
onMouseLeave={(e) => {
|
| 1730 |
+
e.currentTarget.style.transform = 'translateY(-50%) scale(1)'
|
| 1731 |
+
e.currentTarget.style.borderColor = 'var(--copper-medium)'
|
| 1732 |
+
e.currentTarget.style.boxShadow = '0 0 5px var(--copper-bright)'
|
| 1733 |
+
}}
|
| 1734 |
+
style={{
|
| 1735 |
+
position: 'absolute',
|
| 1736 |
+
left: controlsCollapsed ? '8px' : '-30px',
|
| 1737 |
+
top: '50%',
|
| 1738 |
+
transform: 'translateY(-50%)',
|
| 1739 |
+
background: 'linear-gradient(135deg, var(--teal-dark), var(--marble-dark))',
|
| 1740 |
+
border: '2px solid var(--copper-medium)',
|
| 1741 |
+
borderRadius: '50%',
|
| 1742 |
+
width: '28px',
|
| 1743 |
+
height: '28px',
|
| 1744 |
+
color: 'var(--copper-bright)',
|
| 1745 |
+
cursor: 'pointer',
|
| 1746 |
+
fontSize: '12px',
|
| 1747 |
+
display: 'flex',
|
| 1748 |
+
alignItems: 'center',
|
| 1749 |
+
justifyContent: 'center',
|
| 1750 |
+
backdropFilter: 'blur(15px)',
|
| 1751 |
+
transition: 'all 0.4s ease-in-out',
|
| 1752 |
+
zIndex: 1001,
|
| 1753 |
+
fontFamily: 'Uncial Antiqua, serif',
|
| 1754 |
+
boxShadow: '0 0 5px var(--copper-bright)'
|
| 1755 |
+
}}
|
| 1756 |
+
title={controlsCollapsed ? 'Expand Transmission Interface' : 'Collapse Interface'}
|
| 1757 |
+
>
|
| 1758 |
+
{controlsCollapsed ? '⟨' : '⟩'}
|
| 1759 |
+
</button>
|
| 1760 |
+
|
| 1761 |
+
{/* Collapsed State Indicator */}
|
| 1762 |
+
{controlsCollapsed && (
|
| 1763 |
+
<div style={{
|
| 1764 |
+
padding: '12px 8px',
|
| 1765 |
+
textAlign: 'center',
|
| 1766 |
+
fontSize: '9px',
|
| 1767 |
+
lineHeight: '1.1',
|
| 1768 |
+
opacity: 1,
|
| 1769 |
+
transition: 'opacity 0.3s ease-in-out 0.2s',
|
| 1770 |
+
fontFamily: 'Uncial Antiqua, serif'
|
| 1771 |
+
}}>
|
| 1772 |
+
<div className="rune" style={{ fontSize: '14px', color: 'var(--copper-bright)' }}>⟐</div>
|
| 1773 |
+
<div style={{ fontSize: '7px', marginTop: '2px', color: 'var(--copper-medium)' }}>
|
| 1774 |
+
entity<br/>interface
|
| 1775 |
+
</div>
|
| 1776 |
+
</div>
|
| 1777 |
+
)}
|
| 1778 |
+
|
| 1779 |
+
{/* Full Controls Panel */}
|
| 1780 |
+
<div style={{
|
| 1781 |
+
padding: controlsCollapsed ? '0' : '20px 20px 0 20px',
|
| 1782 |
+
opacity: controlsCollapsed ? 0 : 1,
|
| 1783 |
+
transition: 'opacity 0.3s ease-in-out 0.1s',
|
| 1784 |
+
pointerEvents: controlsCollapsed ? 'none' : 'auto',
|
| 1785 |
+
maxHeight: controlsCollapsed ? '0' : 'auto',
|
| 1786 |
+
overflow: 'hidden',
|
| 1787 |
+
flexShrink: 0
|
| 1788 |
+
}}>
|
| 1789 |
+
<h3 className="ancient-title" style={{
|
| 1790 |
+
marginBottom: '10px',
|
| 1791 |
+
fontSize: '16px',
|
| 1792 |
+
textAlign: 'center',
|
| 1793 |
+
fontFamily: 'Metamorphous, serif'
|
| 1794 |
+
}}>
|
| 1795 |
+
<span className="rune">⟐</span> Liminal Sessions <span className="rune">⟐</span>
|
| 1796 |
+
</h3>
|
| 1797 |
+
<div className="transmission-text" style={{
|
| 1798 |
+
textAlign: 'center',
|
| 1799 |
+
fontSize: '9px',
|
| 1800 |
+
marginBottom: '15px',
|
| 1801 |
+
opacity: 0.9,
|
| 1802 |
+
fontFamily: 'Uncial Antiqua, serif'
|
| 1803 |
+
}}>
|
| 1804 |
+
≈ intercepted transmission ≈<br/>
|
| 1805 |
+
◦ audio-reactive entity ◦
|
| 1806 |
+
</div>
|
| 1807 |
+
</div>
|
| 1808 |
+
|
| 1809 |
+
{/* Scrollable Content Area */}
|
| 1810 |
+
<div style={{
|
| 1811 |
+
padding: controlsCollapsed ? '0' : '0 20px 20px 20px',
|
| 1812 |
+
opacity: controlsCollapsed ? 0 : 1,
|
| 1813 |
+
transition: 'opacity 0.3s ease-in-out 0.1s',
|
| 1814 |
+
pointerEvents: controlsCollapsed ? 'none' : 'auto',
|
| 1815 |
+
maxHeight: controlsCollapsed ? '0' : 'calc(75vh - 120px)',
|
| 1816 |
+
overflow: controlsCollapsed ? 'hidden' : 'auto',
|
| 1817 |
+
overflowY: controlsCollapsed ? 'hidden' : 'scroll',
|
| 1818 |
+
flexGrow: 1,
|
| 1819 |
+
// Custom scrollbar styling for better aesthetics
|
| 1820 |
+
scrollbarWidth: 'thin',
|
| 1821 |
+
scrollbarColor: 'var(--copper-medium) transparent'
|
| 1822 |
+
}}
|
| 1823 |
+
// Custom webkit scrollbar styles
|
| 1824 |
+
onMouseEnter={(e) => {
|
| 1825 |
+
e.currentTarget.style.setProperty('--webkit-scrollbar-width', '6px')
|
| 1826 |
+
e.currentTarget.style.setProperty('--webkit-scrollbar-track-background', 'transparent')
|
| 1827 |
+
e.currentTarget.style.setProperty('--webkit-scrollbar-thumb-background', 'var(--copper-medium)')
|
| 1828 |
+
e.currentTarget.style.setProperty('--webkit-scrollbar-thumb-border-radius', '3px')
|
| 1829 |
+
}}
|
| 1830 |
+
>
|
| 1831 |
+
<style>{`
|
| 1832 |
+
.particle-controls::-webkit-scrollbar {
|
| 1833 |
+
width: 6px;
|
| 1834 |
+
}
|
| 1835 |
+
.particle-controls::-webkit-scrollbar-track {
|
| 1836 |
+
background: transparent;
|
| 1837 |
+
}
|
| 1838 |
+
.particle-controls::-webkit-scrollbar-thumb {
|
| 1839 |
+
background: var(--copper-medium);
|
| 1840 |
+
border-radius: 3px;
|
| 1841 |
+
}
|
| 1842 |
+
.particle-controls::-webkit-scrollbar-thumb:hover {
|
| 1843 |
+
background: var(--copper-bright);
|
| 1844 |
+
}
|
| 1845 |
+
`}</style>
|
| 1846 |
+
|
| 1847 |
+
<div style={{ marginBottom: '10px' }}>
|
| 1848 |
+
<label className="runic-text" style={{
|
| 1849 |
+
display: 'block',
|
| 1850 |
+
marginBottom: '5px',
|
| 1851 |
+
fontSize: '11px',
|
| 1852 |
+
color: 'var(--copper-bright)'
|
| 1853 |
+
}}>
|
| 1854 |
+
<span className="rune">◯</span> active entities: {activeSpheresCount}
|
| 1855 |
+
</label>
|
| 1856 |
+
<input
|
| 1857 |
+
type="range"
|
| 1858 |
+
min="1"
|
| 1859 |
+
max="5"
|
| 1860 |
+
value={activeSpheresCount}
|
| 1861 |
+
onChange={(e) => {
|
| 1862 |
+
const count = parseInt(e.target.value)
|
| 1863 |
+
setActiveSpheresCount(count)
|
| 1864 |
+
spheresRef.current.forEach((sphere, index) => {
|
| 1865 |
+
sphere.params.enabled = index < count
|
| 1866 |
+
if (sceneRef.current) {
|
| 1867 |
+
const particleSystem = sceneRef.current.children.find((child: any) =>
|
| 1868 |
+
child.userData?.sphereIndex === sphere.index
|
| 1869 |
+
) as any
|
| 1870 |
+
if (particleSystem) {
|
| 1871 |
+
particleSystem.visible = sphere.params.enabled
|
| 1872 |
+
}
|
| 1873 |
+
}
|
| 1874 |
+
})
|
| 1875 |
+
}}
|
| 1876 |
+
style={{ width: '100%' }}
|
| 1877 |
+
/>
|
| 1878 |
+
</div>
|
| 1879 |
+
|
| 1880 |
+
<div style={{ marginBottom: '10px' }}>
|
| 1881 |
+
<label className="runic-text" style={{
|
| 1882 |
+
display: 'block',
|
| 1883 |
+
marginBottom: '5px',
|
| 1884 |
+
fontSize: '11px',
|
| 1885 |
+
color: 'var(--copper-bright)'
|
| 1886 |
+
}}>
|
| 1887 |
+
<input
|
| 1888 |
+
type="checkbox"
|
| 1889 |
+
checked={cymateGeometry}
|
| 1890 |
+
onChange={(e) => setCymateGeometry(e.target.checked)}
|
| 1891 |
+
style={{ marginRight: '8px' }}
|
| 1892 |
+
/>
|
| 1893 |
+
<span className="rune">◈</span> cymatic patterns
|
| 1894 |
+
</label>
|
| 1895 |
+
</div>
|
| 1896 |
+
|
| 1897 |
+
<div style={{ marginBottom: '10px' }}>
|
| 1898 |
+
<label className="runic-text" style={{
|
| 1899 |
+
display: 'block',
|
| 1900 |
+
marginBottom: '5px',
|
| 1901 |
+
fontSize: '11px',
|
| 1902 |
+
color: 'var(--copper-bright)'
|
| 1903 |
+
}}>
|
| 1904 |
+
<input
|
| 1905 |
+
type="checkbox"
|
| 1906 |
+
checked={tendrilsEnabled}
|
| 1907 |
+
onChange={(e) => setTendrilsEnabled(e.target.checked)}
|
| 1908 |
+
style={{ marginRight: '8px' }}
|
| 1909 |
+
/>
|
| 1910 |
+
<span className="rune">⟨⟩</span> neural tendrils
|
| 1911 |
+
</label>
|
| 1912 |
+
</div>
|
| 1913 |
+
|
| 1914 |
+
<div style={{ marginBottom: '10px' }}>
|
| 1915 |
+
<label className="runic-text" style={{
|
| 1916 |
+
display: 'block',
|
| 1917 |
+
marginBottom: '5px',
|
| 1918 |
+
fontSize: '11px',
|
| 1919 |
+
color: 'var(--copper-bright)'
|
| 1920 |
+
}}>
|
| 1921 |
+
<span className="rune">≋</span> neural density: {Math.round(tendrilDensity * 100)}%
|
| 1922 |
+
</label>
|
| 1923 |
+
<input
|
| 1924 |
+
type="range"
|
| 1925 |
+
min="0.1"
|
| 1926 |
+
max="1.0"
|
| 1927 |
+
step="0.1"
|
| 1928 |
+
value={tendrilDensity}
|
| 1929 |
+
onChange={(e) => setTendrilDensity(parseFloat(e.target.value))}
|
| 1930 |
+
disabled={!tendrilsEnabled}
|
| 1931 |
+
style={{ width: '100%' }}
|
| 1932 |
+
/>
|
| 1933 |
+
</div>
|
| 1934 |
+
|
| 1935 |
+
<div style={{ marginBottom: '10px' }}>
|
| 1936 |
+
<label className="runic-text" style={{
|
| 1937 |
+
display: 'block',
|
| 1938 |
+
marginBottom: '5px',
|
| 1939 |
+
fontSize: '11px',
|
| 1940 |
+
color: 'var(--copper-bright)'
|
| 1941 |
+
}}>
|
| 1942 |
+
<input
|
| 1943 |
+
type="checkbox"
|
| 1944 |
+
checked={dysonSphereEnabled}
|
| 1945 |
+
onChange={(e) => setDysonSphereEnabled(e.target.checked)}
|
| 1946 |
+
style={{ marginRight: '8px' }}
|
| 1947 |
+
/>
|
| 1948 |
+
<span className="rune">🌿</span> growing vines
|
| 1949 |
+
</label>
|
| 1950 |
+
</div>
|
| 1951 |
+
|
| 1952 |
+
<div style={{ marginBottom: '10px' }}>
|
| 1953 |
+
<label className="runic-text" style={{
|
| 1954 |
+
display: 'block',
|
| 1955 |
+
marginBottom: '5px',
|
| 1956 |
+
fontSize: '11px',
|
| 1957 |
+
color: 'var(--copper-bright)'
|
| 1958 |
+
}}>
|
| 1959 |
+
<span className="rune">🌱</span> vine complexity: {vineComplexity}
|
| 1960 |
+
</label>
|
| 1961 |
+
<input
|
| 1962 |
+
type="range"
|
| 1963 |
+
min="8"
|
| 1964 |
+
max="48"
|
| 1965 |
+
step="4"
|
| 1966 |
+
value={vineComplexity}
|
| 1967 |
+
onChange={(e) => setVineComplexity(parseInt(e.target.value))}
|
| 1968 |
+
disabled={!dysonSphereEnabled}
|
| 1969 |
+
style={{ width: '100%' }}
|
| 1970 |
+
/>
|
| 1971 |
+
</div>
|
| 1972 |
+
|
| 1973 |
+
<div style={{ marginBottom: '10px' }}>
|
| 1974 |
+
<label className="runic-text" style={{
|
| 1975 |
+
display: 'block',
|
| 1976 |
+
marginBottom: '5px',
|
| 1977 |
+
fontSize: '11px',
|
| 1978 |
+
color: 'var(--copper-bright)'
|
| 1979 |
+
}}>
|
| 1980 |
+
<span className="rune">⟲</span> growth rate: {Math.round(vineGrowthRate * 1000)}%
|
| 1981 |
+
</label>
|
| 1982 |
+
<input
|
| 1983 |
+
type="range"
|
| 1984 |
+
min="0.005"
|
| 1985 |
+
max="0.05"
|
| 1986 |
+
step="0.005"
|
| 1987 |
+
value={vineGrowthRate}
|
| 1988 |
+
onChange={(e) => setVineGrowthRate(parseFloat(e.target.value))}
|
| 1989 |
+
disabled={!dysonSphereEnabled}
|
| 1990 |
+
style={{ width: '100%' }}
|
| 1991 |
+
/>
|
| 1992 |
+
</div>
|
| 1993 |
+
|
| 1994 |
+
<div style={{ marginBottom: '10px' }}>
|
| 1995 |
+
<label className="runic-text" style={{
|
| 1996 |
+
display: 'block',
|
| 1997 |
+
marginBottom: '5px',
|
| 1998 |
+
fontSize: '11px',
|
| 1999 |
+
color: 'var(--copper-bright)'
|
| 2000 |
+
}}>
|
| 2001 |
+
<input
|
| 2002 |
+
type="checkbox"
|
| 2003 |
+
checked={entronautEnabled}
|
| 2004 |
+
onChange={(e) => setEntronautEnabled(e.target.checked)}
|
| 2005 |
+
style={{ marginRight: '8px' }}
|
| 2006 |
+
/>
|
| 2007 |
+
<span className="rune">⟐</span> entronaut sefa
|
| 2008 |
+
</label>
|
| 2009 |
+
</div>
|
| 2010 |
+
|
| 2011 |
+
<div style={{ marginBottom: '10px' }}>
|
| 2012 |
+
<label className="runic-text" style={{
|
| 2013 |
+
display: 'block',
|
| 2014 |
+
marginBottom: '5px',
|
| 2015 |
+
fontSize: '11px',
|
| 2016 |
+
color: 'var(--copper-bright)'
|
| 2017 |
+
}}>
|
| 2018 |
+
<input
|
| 2019 |
+
type="checkbox"
|
| 2020 |
+
checked={adaptiveCoupling}
|
| 2021 |
+
onChange={(e) => setAdaptiveCoupling(e.target.checked)}
|
| 2022 |
+
disabled={!entronautEnabled || performanceMode}
|
| 2023 |
+
style={{ marginRight: '8px' }}
|
| 2024 |
+
/>
|
| 2025 |
+
<span className="rune">⧨</span> adaptive coupling
|
| 2026 |
+
</label>
|
| 2027 |
+
</div>
|
| 2028 |
+
|
| 2029 |
+
<div style={{ marginBottom: '10px' }}>
|
| 2030 |
+
<label className="runic-text" style={{
|
| 2031 |
+
display: 'block',
|
| 2032 |
+
marginBottom: '5px',
|
| 2033 |
+
fontSize: '11px',
|
| 2034 |
+
color: 'var(--copper-bright)'
|
| 2035 |
+
}}>
|
| 2036 |
+
<input
|
| 2037 |
+
type="checkbox"
|
| 2038 |
+
checked={performanceMode}
|
| 2039 |
+
onChange={(e) => {
|
| 2040 |
+
setPerformanceMode(e.target.checked)
|
| 2041 |
+
if (e.target.checked) {
|
| 2042 |
+
setAdaptiveCoupling(false)
|
| 2043 |
+
setEntronautEnabled(false)
|
| 2044 |
+
}
|
| 2045 |
+
}}
|
| 2046 |
+
style={{ marginRight: '8px' }}
|
| 2047 |
+
/>
|
| 2048 |
+
<span className="rune">⚡</span> performance mode
|
| 2049 |
+
</label>
|
| 2050 |
+
</div>
|
| 2051 |
+
|
| 2052 |
+
{/* Performance Stats Display */}
|
| 2053 |
+
<div style={{
|
| 2054 |
+
marginBottom: '10px',
|
| 2055 |
+
padding: '8px',
|
| 2056 |
+
background: 'rgba(0,0,0,0.3)',
|
| 2057 |
+
borderRadius: '4px',
|
| 2058 |
+
border: '1px solid var(--copper-dark)'
|
| 2059 |
+
}}>
|
| 2060 |
+
<div className="runic-text" style={{
|
| 2061 |
+
fontSize: '9px',
|
| 2062 |
+
lineHeight: '1.3',
|
| 2063 |
+
color: 'var(--copper-medium)'
|
| 2064 |
+
}}>
|
| 2065 |
+
<div><span className="rune">⚡</span> quality: 100%</div>
|
| 2066 |
+
<div><span className="rune">🖥️</span> particles: {performanceStats.particleCount.toLocaleString()}</div>
|
| 2067 |
+
<div style={{
|
| 2068 |
+
color: performanceStats.fps < 30 ? 'var(--error-rust)' :
|
| 2069 |
+
performanceStats.fps < 45 ? 'var(--warning-copper)' : 'var(--transmission-glow)'
|
| 2070 |
+
}}>
|
| 2071 |
+
<span className="rune">📊</span> performance: {performanceStats.fps >= 45 ? 'optimal' : performanceStats.fps >= 30 ? 'good' : 'stressed'}
|
| 2072 |
+
</div>
|
| 2073 |
+
</div>
|
| 2074 |
+
</div>
|
| 2075 |
+
|
| 2076 |
+
<div style={{ marginBottom: '10px' }}>
|
| 2077 |
+
<label style={{ display: 'block', marginBottom: '5px', fontSize: '12px' }}>
|
| 2078 |
+
<input
|
| 2079 |
+
type="checkbox"
|
| 2080 |
+
checked={cameraControlsEnabled}
|
| 2081 |
+
onChange={(e) => setCameraControlsEnabled(e.target.checked)}
|
| 2082 |
+
style={{ marginRight: '8px' }}
|
| 2083 |
+
/>
|
| 2084 |
+
Camera Controls
|
| 2085 |
+
</label>
|
| 2086 |
+
</div>
|
| 2087 |
+
|
| 2088 |
+
<div style={{ marginBottom: '10px' }}>
|
| 2089 |
+
<label style={{ display: 'block', marginBottom: '5px', fontSize: '12px' }}>
|
| 2090 |
+
<input
|
| 2091 |
+
type="checkbox"
|
| 2092 |
+
checked={autoRotate}
|
| 2093 |
+
onChange={(e) => setAutoRotate(e.target.checked)}
|
| 2094 |
+
disabled={!cameraControlsEnabled}
|
| 2095 |
+
style={{ marginRight: '8px' }}
|
| 2096 |
+
/>
|
| 2097 |
+
Auto Rotate
|
| 2098 |
+
</label>
|
| 2099 |
+
</div>
|
| 2100 |
+
|
| 2101 |
+
{/* Camera Preset Buttons */}
|
| 2102 |
+
{cameraControlsEnabled && (
|
| 2103 |
+
<div style={{ marginBottom: '10px' }}>
|
| 2104 |
+
<div style={{ fontSize: '11px', marginBottom: '5px', color: '#FFD700' }}>Camera Presets:</div>
|
| 2105 |
+
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '4px' }}>
|
| 2106 |
+
<button
|
| 2107 |
+
onClick={() => setCameraPreset('default')}
|
| 2108 |
+
style={{
|
| 2109 |
+
background: 'rgba(255, 215, 0, 0.2)',
|
| 2110 |
+
border: '1px solid rgba(255, 215, 0, 0.4)',
|
| 2111 |
+
color: '#FFD700',
|
| 2112 |
+
padding: '4px 8px',
|
| 2113 |
+
borderRadius: '4px',
|
| 2114 |
+
fontSize: '9px',
|
| 2115 |
+
cursor: 'pointer'
|
| 2116 |
+
}}
|
| 2117 |
+
>
|
| 2118 |
+
Default
|
| 2119 |
+
</button>
|
| 2120 |
+
<button
|
| 2121 |
+
onClick={() => setCameraPreset('inside')}
|
| 2122 |
+
style={{
|
| 2123 |
+
background: 'rgba(255, 215, 0, 0.2)',
|
| 2124 |
+
border: '1px solid rgba(255, 215, 0, 0.4)',
|
| 2125 |
+
color: '#FFD700',
|
| 2126 |
+
padding: '4px 8px',
|
| 2127 |
+
borderRadius: '4px',
|
| 2128 |
+
fontSize: '9px',
|
| 2129 |
+
cursor: 'pointer'
|
| 2130 |
+
}}
|
| 2131 |
+
>
|
| 2132 |
+
Inside
|
| 2133 |
+
</button>
|
| 2134 |
+
<button
|
| 2135 |
+
onClick={() => setCameraPreset('far')}
|
| 2136 |
+
style={{
|
| 2137 |
+
background: 'rgba(255, 215, 0, 0.2)',
|
| 2138 |
+
border: '1px solid rgba(255, 215, 0, 0.4)',
|
| 2139 |
+
color: '#FFD700',
|
| 2140 |
+
padding: '4px 8px',
|
| 2141 |
+
borderRadius: '4px',
|
| 2142 |
+
fontSize: '9px',
|
| 2143 |
+
cursor: 'pointer'
|
| 2144 |
+
}}
|
| 2145 |
+
>
|
| 2146 |
+
Far View
|
| 2147 |
+
</button>
|
| 2148 |
+
<button
|
| 2149 |
+
onClick={() => setCameraPreset('top')}
|
| 2150 |
+
style={{
|
| 2151 |
+
background: 'rgba(255, 215, 0, 0.2)',
|
| 2152 |
+
border: '1px solid rgba(255, 215, 0, 0.4)',
|
| 2153 |
+
color: '#FFD700',
|
| 2154 |
+
padding: '4px 8px',
|
| 2155 |
+
borderRadius: '4px',
|
| 2156 |
+
fontSize: '9px',
|
| 2157 |
+
cursor: 'pointer'
|
| 2158 |
+
}}
|
| 2159 |
+
>
|
| 2160 |
+
Top View
|
| 2161 |
+
</button>
|
| 2162 |
+
</div>
|
| 2163 |
+
<button
|
| 2164 |
+
onClick={resetCameraPosition}
|
| 2165 |
+
style={{
|
| 2166 |
+
background: 'rgba(255, 215, 0, 0.3)',
|
| 2167 |
+
border: '1px solid rgba(255, 215, 0, 0.5)',
|
| 2168 |
+
color: '#FFD700',
|
| 2169 |
+
padding: '4px 8px',
|
| 2170 |
+
borderRadius: '4px',
|
| 2171 |
+
fontSize: '9px',
|
| 2172 |
+
cursor: 'pointer',
|
| 2173 |
+
width: '100%',
|
| 2174 |
+
marginTop: '4px'
|
| 2175 |
+
}}
|
| 2176 |
+
>
|
| 2177 |
+
Reset Camera
|
| 2178 |
+
</button>
|
| 2179 |
+
</div>
|
| 2180 |
+
)}
|
| 2181 |
+
|
| 2182 |
+
<div style={{ marginBottom: '10px' }}>
|
| 2183 |
+
<label style={{ display: 'block', marginBottom: '5px', fontSize: '12px' }}>
|
| 2184 |
+
<input
|
| 2185 |
+
type="checkbox"
|
| 2186 |
+
checked={fogParams.enabled}
|
| 2187 |
+
onChange={async (e) => {
|
| 2188 |
+
setFogParams(prev => ({ ...prev, enabled: e.target.checked }))
|
| 2189 |
+
const THREE = await import('three')
|
| 2190 |
+
updateFog(THREE)
|
| 2191 |
+
}}
|
| 2192 |
+
style={{ marginRight: '8px' }}
|
| 2193 |
+
/>
|
| 2194 |
+
Fog Effect
|
| 2195 |
+
</label>
|
| 2196 |
+
</div>
|
| 2197 |
+
|
| 2198 |
+
<div className="transmission-text" style={{
|
| 2199 |
+
fontSize: '9px',
|
| 2200 |
+
opacity: 0.9,
|
| 2201 |
+
marginTop: '15px',
|
| 2202 |
+
lineHeight: '1.4',
|
| 2203 |
+
fontFamily: 'Uncial Antiqua, serif',
|
| 2204 |
+
color: 'var(--copper-medium)'
|
| 2205 |
+
}}>
|
| 2206 |
+
<div>◦ audio-reactive ◦ | ≋ beat-detection ≋ | ⟐ dynamic-noise ⟐</div>
|
| 2207 |
+
<div style={{ marginTop: '5px' }}>
|
| 2208 |
+
<span className="rune">◯</span> {spheresRef.current.filter(s => s.params.enabled).length} entities |
|
| 2209 |
+
<span className="rune">✦</span> {performanceStats.particleCount.toLocaleString()} particles
|
| 2210 |
+
</div>
|
| 2211 |
+
<div style={{
|
| 2212 |
+
marginTop: '5px',
|
| 2213 |
+
color: performanceStats.fps < 30 ? 'var(--error-rust)' : performanceStats.fps < 45 ? 'var(--warning-copper)' : 'var(--transmission-glow)'
|
| 2214 |
+
}}>
|
| 2215 |
+
<span className="rune">⧨</span> {performanceStats.fps} fps | transmission {performanceStats.fps >= 45 ? 'stable' : performanceStats.fps >= 30 ? 'fluctuating' : 'unstable'}
|
| 2216 |
+
</div>
|
| 2217 |
+
{cymateGeometry && <div style={{ marginTop: '3px' }}><span className="rune">◈</span> cymatic patterns active</div>}
|
| 2218 |
+
{tendrilsEnabled && <div style={{ marginTop: '3px' }}><span className="rune">⟨⟩</span> neural tendrils ({Math.round(tendrilDensity * 100)}% density)</div>}
|
| 2219 |
+
{dysonSphereEnabled && <div style={{ marginTop: '3px' }}><span className="rune">🌿</span> growing vines ({vineComplexity} complexity, {Math.round(vineGrowthRate * 1000)}% growth)</div>}
|
| 2220 |
+
{cameraControlsEnabled && (
|
| 2221 |
+
<div style={{ marginTop: '3px', fontSize: '8px', color: 'var(--teal-light)' }}>
|
| 2222 |
+
<span className="rune">⟐</span> observer: orbit | zoom | pan
|
| 2223 |
+
</div>
|
| 2224 |
+
)}
|
| 2225 |
+
{entronautEnabled && (
|
| 2226 |
+
<div style={{ marginTop: '5px', fontSize: '8px', lineHeight: '1.3' }}>
|
| 2227 |
+
<div><span className="rune">⟐</span> sefa analysis: {entronautStateRef.current ? 'active' : 'initializing'}</div>
|
| 2228 |
+
{entronautStateRef.current && (
|
| 2229 |
+
<>
|
| 2230 |
+
<div>complexity: {(entronautStateRef.current.informationMetrics.complexity * 100).toFixed(1)}%</div>
|
| 2231 |
+
<div>emergence: {(entronautStateRef.current.informationMetrics.emergence * 100).toFixed(1)}%</div>
|
| 2232 |
+
<div>coherence: {(entronautStateRef.current.informationMetrics.coherence * 100).toFixed(1)}%</div>
|
| 2233 |
+
{adaptiveCoupling && (
|
| 2234 |
+
<div style={{ marginTop: '3px', color: 'var(--teal-light)' }}>
|
| 2235 |
+
<span className="rune">⧨</span> biomimetic coupling active
|
| 2236 |
+
</div>
|
| 2237 |
+
)}
|
| 2238 |
+
</>
|
| 2239 |
+
)}
|
| 2240 |
+
</div>
|
| 2241 |
+
)}
|
| 2242 |
+
<div style={{ marginTop: '5px', fontSize: '8px', opacity: 0.8 }}>
|
| 2243 |
+
<div>sub-bass (20-80hz) | bass (120-250hz) | mid (250-800hz)</div>
|
| 2244 |
+
<div>high-mid (1k-4khz) | high (5k-10khz)</div>
|
| 2245 |
+
</div>
|
| 2246 |
+
</div>
|
| 2247 |
+
</div>
|
| 2248 |
+
</div>
|
| 2249 |
+
|
| 2250 |
+
{/* About Section - Clickable Widget */}
|
| 2251 |
+
<div
|
| 2252 |
+
className="about-section ancient-panel"
|
| 2253 |
+
onClick={() => setAboutCollapsed(!aboutCollapsed)}
|
| 2254 |
+
style={{
|
| 2255 |
+
position: 'fixed',
|
| 2256 |
+
bottom: '20px',
|
| 2257 |
+
left: '50%',
|
| 2258 |
+
transform: aboutCollapsed ? 'translateX(-50%) translateY(calc(100% - 65px))' : 'translateX(-50%) translateY(0)',
|
| 2259 |
+
zIndex: 1000,
|
| 2260 |
+
background: 'linear-gradient(135deg, var(--marble-dark), var(--teal-dark))',
|
| 2261 |
+
borderRadius: '10px',
|
| 2262 |
+
color: 'var(--copper-light)',
|
| 2263 |
+
fontFamily: 'Metamorphous, serif',
|
| 2264 |
+
width: '380px',
|
| 2265 |
+
minWidth: '380px',
|
| 2266 |
+
transition: 'all 0.4s ease-in-out',
|
| 2267 |
+
backdropFilter: 'blur(20px)',
|
| 2268 |
+
border: '2px solid var(--copper-medium)',
|
| 2269 |
+
pointerEvents: 'auto',
|
| 2270 |
+
boxShadow: '0 4px 20px var(--copper-dark), inset 0 0 15px var(--teal-dark)',
|
| 2271 |
+
cursor: 'pointer'
|
| 2272 |
+
}}
|
| 2273 |
+
onMouseEnter={(e) => {
|
| 2274 |
+
// Disable camera controls when mouse enters about section
|
| 2275 |
+
const event = new CustomEvent('disableCameraControls', { detail: true })
|
| 2276 |
+
window.dispatchEvent(event)
|
| 2277 |
+
// Add hover effect
|
| 2278 |
+
e.currentTarget.style.borderColor = 'var(--transmission-glow)'
|
| 2279 |
+
e.currentTarget.style.boxShadow = '0 6px 25px var(--transmission-glow)'
|
| 2280 |
+
}}
|
| 2281 |
+
onMouseLeave={(e) => {
|
| 2282 |
+
// Re-enable camera controls when mouse leaves about section
|
| 2283 |
+
const event = new CustomEvent('disableCameraControls', { detail: false })
|
| 2284 |
+
window.dispatchEvent(event)
|
| 2285 |
+
// Remove hover effect
|
| 2286 |
+
e.currentTarget.style.borderColor = 'var(--copper-medium)'
|
| 2287 |
+
e.currentTarget.style.boxShadow = '0 4px 20px var(--copper-dark)'
|
| 2288 |
+
}}
|
| 2289 |
+
>
|
| 2290 |
+
{/* Collapsed State - Clickable Widget */}
|
| 2291 |
+
{aboutCollapsed && (
|
| 2292 |
+
<div style={{
|
| 2293 |
+
padding: '15px 20px',
|
| 2294 |
+
textAlign: 'center',
|
| 2295 |
+
fontSize: '11px',
|
| 2296 |
+
lineHeight: '1.3',
|
| 2297 |
+
display: 'flex',
|
| 2298 |
+
alignItems: 'center',
|
| 2299 |
+
justifyContent: 'center',
|
| 2300 |
+
gap: '10px',
|
| 2301 |
+
minHeight: '50px'
|
| 2302 |
+
}}>
|
| 2303 |
+
<div className="rune" style={{ fontSize: '18px', color: 'var(--copper-bright)' }}>⟐</div>
|
| 2304 |
+
<div style={{
|
| 2305 |
+
flex: 1,
|
| 2306 |
+
display: 'flex',
|
| 2307 |
+
flexDirection: 'column',
|
| 2308 |
+
alignItems: 'center',
|
| 2309 |
+
gap: '2px'
|
| 2310 |
+
}}>
|
| 2311 |
+
<div className="ancient-title" style={{
|
| 2312 |
+
fontSize: '11px',
|
| 2313 |
+
color: 'var(--copper-bright)',
|
| 2314 |
+
fontWeight: 'bold',
|
| 2315 |
+
fontFamily: 'Metamorphous, serif'
|
| 2316 |
+
}}>
|
| 2317 |
+
transmission source
|
| 2318 |
+
</div>
|
| 2319 |
+
<div className="runic-text" style={{
|
| 2320 |
+
fontSize: '8px',
|
| 2321 |
+
color: 'var(--copper-medium)',
|
| 2322 |
+
opacity: 0.9,
|
| 2323 |
+
fontFamily: 'Uncial Antiqua, serif'
|
| 2324 |
+
}}>
|
| 2325 |
+
◦ access entity data ◦
|
| 2326 |
+
</div>
|
| 2327 |
+
</div>
|
| 2328 |
+
<div className="rune" style={{ fontSize: '12px', opacity: 0.7, color: 'var(--transmission-glow)' }}>⟨</div>
|
| 2329 |
+
</div>
|
| 2330 |
+
)}
|
| 2331 |
+
|
| 2332 |
+
{/* Expanded State - Full About Panel */}
|
| 2333 |
+
{!aboutCollapsed && (
|
| 2334 |
+
<div style={{
|
| 2335 |
+
padding: '20px',
|
| 2336 |
+
transition: 'all 0.3s ease-in-out'
|
| 2337 |
+
}}>
|
| 2338 |
+
<div style={{
|
| 2339 |
+
display: 'flex',
|
| 2340 |
+
alignItems: 'center',
|
| 2341 |
+
justifyContent: 'space-between',
|
| 2342 |
+
marginBottom: '15px'
|
| 2343 |
+
}}>
|
| 2344 |
+
<h3 className="ancient-title" style={{
|
| 2345 |
+
margin: 0,
|
| 2346 |
+
fontSize: '15px',
|
| 2347 |
+
textAlign: 'center',
|
| 2348 |
+
flex: 1,
|
| 2349 |
+
fontFamily: 'Metamorphous, serif',
|
| 2350 |
+
color: 'var(--copper-bright)'
|
| 2351 |
+
}}>
|
| 2352 |
+
<span className="rune">⟐</span> Liminal Sessions <span className="rune">⟐</span>
|
| 2353 |
+
</h3>
|
| 2354 |
+
<div className="rune" style={{ fontSize: '12px', opacity: 0.7, color: 'var(--transmission-glow)' }}>⟩</div>
|
| 2355 |
+
</div>
|
| 2356 |
+
|
| 2357 |
+
<div className="transmission-text" style={{
|
| 2358 |
+
fontSize: '9px',
|
| 2359 |
+
lineHeight: '1.6',
|
| 2360 |
+
marginBottom: '12px',
|
| 2361 |
+
fontFamily: 'Uncial Antiqua, serif',
|
| 2362 |
+
color: 'var(--copper-medium)'
|
| 2363 |
+
}}>
|
| 2364 |
+
<p style={{ marginBottom: '8px' }}>
|
| 2365 |
+
≈ threshold frequencies between electronic and organic realms ≈
|
| 2366 |
+
</p>
|
| 2367 |
+
|
| 2368 |
+
<p style={{ marginBottom: '8px' }}>
|
| 2369 |
+
◦ biomimetic algorithms manifest living visual entities ◦<br/>
|
| 2370 |
+
responding to spectral emergence patterns
|
| 2371 |
+
</p>
|
| 2372 |
+
|
| 2373 |
+
<p style={{ fontSize: '8px', opacity: 0.8 }}>
|
| 2374 |
+
[ transmission intercepted from distant coordinate ]
|
| 2375 |
+
</p>
|
| 2376 |
+
</div>
|
| 2377 |
+
|
| 2378 |
+
<div className="runic-text" style={{
|
| 2379 |
+
fontSize: '8px',
|
| 2380 |
+
opacity: 0.9,
|
| 2381 |
+
lineHeight: '1.4',
|
| 2382 |
+
fontFamily: 'Uncial Antiqua, serif',
|
| 2383 |
+
color: 'var(--copper-bright)'
|
| 2384 |
+
}}>
|
| 2385 |
+
<div><span className="rune">♪</span> ambient electronic resonance</div>
|
| 2386 |
+
<div><span className="rune">◯</span> audio-visual emergence field</div>
|
| 2387 |
+
<div><span className="rune">≋</span> real-time particle dynamics</div>
|
| 2388 |
+
</div>
|
| 2389 |
+
</div>
|
| 2390 |
+
)}
|
| 2391 |
+
</div>
|
| 2392 |
+
</div>
|
| 2393 |
+
)
|
| 2394 |
+
}
|
src/engine/AudioEngine.ts
ADDED
|
@@ -0,0 +1,665 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type {
|
| 2 |
+
AudioData,
|
| 3 |
+
AudioEngine as AudioEngineInterface,
|
| 4 |
+
Track,
|
| 5 |
+
PeakDetection,
|
| 6 |
+
BeatManager,
|
| 7 |
+
FrequencyMapping
|
| 8 |
+
} from '../types/audio'
|
| 9 |
+
|
| 10 |
+
export class AudioEngine implements AudioEngineInterface {
|
| 11 |
+
private audioContext: AudioContext | null = null
|
| 12 |
+
private analyser: AnalyserNode | null = null
|
| 13 |
+
private audioElement: HTMLAudioElement | null = null
|
| 14 |
+
private sourceNode: MediaElementAudioSourceNode | null = null
|
| 15 |
+
private isInitialized = false
|
| 16 |
+
private isContextStarted = false
|
| 17 |
+
private currentTrack: Track | null = null
|
| 18 |
+
|
| 19 |
+
// Auto-play functionality
|
| 20 |
+
private onTrackEndedCallback: (() => void) | null = null
|
| 21 |
+
|
| 22 |
+
// Audio analysis configuration
|
| 23 |
+
private readonly frequencyMapping: FrequencyMapping = {
|
| 24 |
+
deepFreq: { min: 20, max: 250 }, // Deep Earth Pulse (trunk/roots)
|
| 25 |
+
midFreq: { min: 250, max: 4000 }, // Heartwood Resonance (core glyphs)
|
| 26 |
+
highFreq: { min: 4000, max: 20000 } // Canopy Shiver (fractal branches)
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
private peakDetection: PeakDetection = {
|
| 30 |
+
energyHistory: [],
|
| 31 |
+
historyLength: 30,
|
| 32 |
+
lastPeakTime: 0,
|
| 33 |
+
minTimeBetweenPeaks: 200,
|
| 34 |
+
sensitivity: 1.1
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
private beatManager: BeatManager = {
|
| 38 |
+
currentWaveRadius: 0,
|
| 39 |
+
waveStrength: 0,
|
| 40 |
+
isWaveActive: false,
|
| 41 |
+
|
| 42 |
+
triggerWave: (energy: number) => {
|
| 43 |
+
const maxEnergy = 255
|
| 44 |
+
const energyExcess = energy - 200
|
| 45 |
+
this.beatManager.waveStrength = (energyExcess / (maxEnergy - 200)) * 20.0
|
| 46 |
+
this.beatManager.currentWaveRadius = 0
|
| 47 |
+
this.beatManager.isWaveActive = true
|
| 48 |
+
},
|
| 49 |
+
|
| 50 |
+
update: (deltaTime: number) => {
|
| 51 |
+
if (this.beatManager.isWaveActive) {
|
| 52 |
+
this.beatManager.currentWaveRadius += deltaTime * 1.0
|
| 53 |
+
this.beatManager.waveStrength *= 0.98
|
| 54 |
+
|
| 55 |
+
if (this.beatManager.currentWaveRadius > 1.0 || this.beatManager.waveStrength < 0.1) {
|
| 56 |
+
this.beatManager.isWaveActive = false
|
| 57 |
+
}
|
| 58 |
+
}
|
| 59 |
+
},
|
| 60 |
+
|
| 61 |
+
getWaveForce: (distance: number) => {
|
| 62 |
+
if (!this.beatManager.isWaveActive) return 0
|
| 63 |
+
const distanceFromWave = Math.abs(distance - this.beatManager.currentWaveRadius)
|
| 64 |
+
if (distanceFromWave < 0.1) {
|
| 65 |
+
return this.beatManager.waveStrength * Math.exp(-distanceFromWave * 10)
|
| 66 |
+
}
|
| 67 |
+
return 0
|
| 68 |
+
}
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
async initialize(): Promise<void> {
|
| 72 |
+
if (this.isInitialized) return
|
| 73 |
+
|
| 74 |
+
try {
|
| 75 |
+
// Create AudioContext but don't start it yet (browser security)
|
| 76 |
+
this.audioContext = new (window.AudioContext || (window as any).webkitAudioContext)()
|
| 77 |
+
this.analyser = this.audioContext.createAnalyser()
|
| 78 |
+
this.analyser.fftSize = 2048
|
| 79 |
+
|
| 80 |
+
// Don't resume context here - wait for user gesture
|
| 81 |
+
this.isInitialized = true
|
| 82 |
+
console.log('AudioEngine initialized (context suspended until user gesture)')
|
| 83 |
+
} catch (error) {
|
| 84 |
+
console.error('Failed to initialize AudioEngine:', error)
|
| 85 |
+
throw error
|
| 86 |
+
}
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
private async ensureContextStarted(): Promise<void> {
|
| 90 |
+
if (!this.audioContext || this.isContextStarted) return
|
| 91 |
+
|
| 92 |
+
if (this.audioContext.state === 'suspended') {
|
| 93 |
+
try {
|
| 94 |
+
await this.audioContext.resume()
|
| 95 |
+
this.isContextStarted = true
|
| 96 |
+
console.log('AudioContext resumed after user gesture')
|
| 97 |
+
} catch (error) {
|
| 98 |
+
console.error('Failed to resume AudioContext:', error)
|
| 99 |
+
throw error
|
| 100 |
+
}
|
| 101 |
+
} else {
|
| 102 |
+
this.isContextStarted = true
|
| 103 |
+
}
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
async loadTrack(track: Track): Promise<void> {
|
| 107 |
+
if (!this.isInitialized) {
|
| 108 |
+
throw new Error('AudioEngine not initialized')
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
try {
|
| 112 |
+
console.log(`🎵 Loading track: ${track.title} from ${track.url}`)
|
| 113 |
+
|
| 114 |
+
// Test if the file is accessible
|
| 115 |
+
console.log('🔍 Testing file accessibility...')
|
| 116 |
+
try {
|
| 117 |
+
const response = await fetch(track.url, { method: 'HEAD' })
|
| 118 |
+
if (!response.ok) {
|
| 119 |
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
| 120 |
+
}
|
| 121 |
+
console.log('✅ File is accessible via HTTP')
|
| 122 |
+
console.log('📊 Response headers:', {
|
| 123 |
+
contentType: response.headers.get('content-type'),
|
| 124 |
+
contentLength: response.headers.get('content-length'),
|
| 125 |
+
status: response.status
|
| 126 |
+
})
|
| 127 |
+
} catch (fetchError) {
|
| 128 |
+
console.error('❌ File accessibility test failed:', fetchError)
|
| 129 |
+
throw new Error(`Cannot access audio file: ${fetchError}`)
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
// Create audio element if it doesn't exist
|
| 133 |
+
if (!this.audioElement) {
|
| 134 |
+
console.log('🎧 Creating new audio element')
|
| 135 |
+
this.audioElement = document.createElement('audio')
|
| 136 |
+
this.audioElement.crossOrigin = 'anonymous'
|
| 137 |
+
this.audioElement.preload = 'metadata'
|
| 138 |
+
this.audioElement.volume = 0.7 // Set initial volume
|
| 139 |
+
|
| 140 |
+
console.log('🔊 Audio element volume set to:', this.audioElement.volume)
|
| 141 |
+
|
| 142 |
+
// Add comprehensive event listeners
|
| 143 |
+
this.audioElement.addEventListener('loadedmetadata', () => {
|
| 144 |
+
console.log(`✅ Track metadata loaded: ${this.audioElement?.duration}s`)
|
| 145 |
+
})
|
| 146 |
+
|
| 147 |
+
this.audioElement.addEventListener('timeupdate', () => {
|
| 148 |
+
// This enables real-time current time tracking
|
| 149 |
+
})
|
| 150 |
+
|
| 151 |
+
this.audioElement.addEventListener('ended', () => {
|
| 152 |
+
console.log('🔚 Track ended:', this.currentTrack?.title)
|
| 153 |
+
// Trigger auto-play callback if set
|
| 154 |
+
if (this.onTrackEndedCallback) {
|
| 155 |
+
console.log('🎵 Triggering auto-play to next track')
|
| 156 |
+
this.onTrackEndedCallback()
|
| 157 |
+
}
|
| 158 |
+
})
|
| 159 |
+
|
| 160 |
+
this.audioElement.addEventListener('error', (e) => {
|
| 161 |
+
console.error('❌ Audio error:', e)
|
| 162 |
+
console.error('Error code:', this.audioElement?.error?.code)
|
| 163 |
+
console.error('Error message:', this.audioElement?.error?.message)
|
| 164 |
+
})
|
| 165 |
+
|
| 166 |
+
this.audioElement.addEventListener('canplaythrough', () => {
|
| 167 |
+
console.log('✅ Track can play through')
|
| 168 |
+
})
|
| 169 |
+
|
| 170 |
+
this.audioElement.addEventListener('loadstart', () => {
|
| 171 |
+
console.log('🔄 Started loading track')
|
| 172 |
+
})
|
| 173 |
+
|
| 174 |
+
this.audioElement.addEventListener('progress', () => {
|
| 175 |
+
console.log('📊 Loading progress')
|
| 176 |
+
})
|
| 177 |
+
|
| 178 |
+
this.audioElement.addEventListener('canplay', () => {
|
| 179 |
+
console.log('✅ Track can start playing')
|
| 180 |
+
})
|
| 181 |
+
|
| 182 |
+
this.audioElement.addEventListener('stalled', () => {
|
| 183 |
+
console.warn('⚠️ Track loading stalled')
|
| 184 |
+
})
|
| 185 |
+
|
| 186 |
+
this.audioElement.addEventListener('suspend', () => {
|
| 187 |
+
console.warn('⚠️ Track loading suspended')
|
| 188 |
+
})
|
| 189 |
+
|
| 190 |
+
this.audioElement.addEventListener('abort', () => {
|
| 191 |
+
console.warn('⚠️ Track loading aborted')
|
| 192 |
+
})
|
| 193 |
+
|
| 194 |
+
console.log('✅ Audio element created with event listeners')
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
// Load the new track
|
| 198 |
+
console.log('📝 Setting audio source...')
|
| 199 |
+
this.audioElement.src = track.url
|
| 200 |
+
this.currentTrack = track
|
| 201 |
+
|
| 202 |
+
console.log('📊 Audio element initial state:', {
|
| 203 |
+
src: this.audioElement.src,
|
| 204 |
+
readyState: this.audioElement.readyState,
|
| 205 |
+
networkState: this.audioElement.networkState
|
| 206 |
+
})
|
| 207 |
+
|
| 208 |
+
// Wait for metadata to load
|
| 209 |
+
return new Promise((resolve, reject) => {
|
| 210 |
+
const timeout = setTimeout(() => {
|
| 211 |
+
this.audioElement?.removeEventListener('loadedmetadata', onLoadedMetadata)
|
| 212 |
+
this.audioElement?.removeEventListener('error', onError)
|
| 213 |
+
console.error('❌ Track loading timeout after 10 seconds')
|
| 214 |
+
reject(new Error('Track loading timeout after 10 seconds'))
|
| 215 |
+
}, 10000)
|
| 216 |
+
|
| 217 |
+
const onLoadedMetadata = () => {
|
| 218 |
+
clearTimeout(timeout)
|
| 219 |
+
this.audioElement?.removeEventListener('loadedmetadata', onLoadedMetadata)
|
| 220 |
+
this.audioElement?.removeEventListener('error', onError)
|
| 221 |
+
console.log(`✅ Track loaded successfully: ${track.title}`)
|
| 222 |
+
console.log('📊 Final audio element state:', {
|
| 223 |
+
duration: this.audioElement?.duration,
|
| 224 |
+
readyState: this.audioElement?.readyState,
|
| 225 |
+
networkState: this.audioElement?.networkState
|
| 226 |
+
})
|
| 227 |
+
resolve()
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
const onError = () => {
|
| 231 |
+
clearTimeout(timeout)
|
| 232 |
+
this.audioElement?.removeEventListener('loadedmetadata', onLoadedMetadata)
|
| 233 |
+
this.audioElement?.removeEventListener('error', onError)
|
| 234 |
+
console.error(`❌ Failed to load track: ${track.title}`)
|
| 235 |
+
console.error('❌ Audio element error state:', {
|
| 236 |
+
error: this.audioElement?.error,
|
| 237 |
+
readyState: this.audioElement?.readyState,
|
| 238 |
+
networkState: this.audioElement?.networkState
|
| 239 |
+
})
|
| 240 |
+
reject(new Error('Failed to load audio track'))
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
console.log('⏳ Waiting for metadata to load...')
|
| 244 |
+
this.audioElement?.addEventListener('loadedmetadata', onLoadedMetadata)
|
| 245 |
+
this.audioElement?.addEventListener('error', onError)
|
| 246 |
+
this.audioElement?.load()
|
| 247 |
+
})
|
| 248 |
+
} catch (error) {
|
| 249 |
+
console.error('❌ Failed to load track:', error)
|
| 250 |
+
throw error
|
| 251 |
+
}
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
async play(): Promise<void> {
|
| 255 |
+
if (!this.audioElement || !this.currentTrack) {
|
| 256 |
+
throw new Error('No track loaded')
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
console.log(`🎵 Attempting to play: ${this.currentTrack.title}`)
|
| 260 |
+
console.log(`📊 Audio element state:`, {
|
| 261 |
+
src: this.audioElement.src,
|
| 262 |
+
readyState: this.audioElement.readyState,
|
| 263 |
+
paused: this.audioElement.paused,
|
| 264 |
+
duration: this.audioElement.duration,
|
| 265 |
+
currentTime: this.audioElement.currentTime,
|
| 266 |
+
volume: this.audioElement.volume
|
| 267 |
+
})
|
| 268 |
+
|
| 269 |
+
try {
|
| 270 |
+
// Ensure AudioContext is started (user gesture required)
|
| 271 |
+
await this.ensureContextStarted()
|
| 272 |
+
|
| 273 |
+
// Create audio source connection if not already done
|
| 274 |
+
if (!this.sourceNode && this.audioContext && this.analyser && this.isContextStarted) {
|
| 275 |
+
console.log('🔗 Connecting audio source to analyser')
|
| 276 |
+
this.sourceNode = this.audioContext.createMediaElementSource(this.audioElement)
|
| 277 |
+
this.sourceNode.connect(this.analyser)
|
| 278 |
+
this.analyser.connect(this.audioContext.destination)
|
| 279 |
+
console.log('✅ Audio source connected to analyser')
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
console.log('▶️ Calling audio.play()')
|
| 283 |
+
await this.audioElement.play()
|
| 284 |
+
console.log('✅ Playback started successfully')
|
| 285 |
+
} catch (error) {
|
| 286 |
+
console.error('❌ Playback failed:', error)
|
| 287 |
+
if (error instanceof DOMException) {
|
| 288 |
+
console.error('DOMException details:', {
|
| 289 |
+
name: error.name,
|
| 290 |
+
message: error.message,
|
| 291 |
+
code: error.code
|
| 292 |
+
})
|
| 293 |
+
}
|
| 294 |
+
throw error
|
| 295 |
+
}
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
pause(): void {
|
| 299 |
+
if (this.audioElement) {
|
| 300 |
+
this.audioElement.pause()
|
| 301 |
+
console.log('Playback paused')
|
| 302 |
+
}
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
seekTo(time: number): void {
|
| 306 |
+
if (this.audioElement) {
|
| 307 |
+
this.audioElement.currentTime = time
|
| 308 |
+
}
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
setVolume(volume: number): void {
|
| 312 |
+
if (this.audioElement) {
|
| 313 |
+
this.audioElement.volume = Math.max(0, Math.min(1, volume))
|
| 314 |
+
}
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
getAudioData(): AudioData {
|
| 318 |
+
if (!this.analyser || !this.audioContext) {
|
| 319 |
+
return {
|
| 320 |
+
frequencies: new Uint8Array(1024),
|
| 321 |
+
deepEnergy: 0,
|
| 322 |
+
midEnergy: 0,
|
| 323 |
+
highEnergy: 0,
|
| 324 |
+
overallAmplitude: 0,
|
| 325 |
+
peakDetected: false,
|
| 326 |
+
beatDetected: false
|
| 327 |
+
}
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
try {
|
| 331 |
+
const frequencies = new Uint8Array(this.analyser.frequencyBinCount)
|
| 332 |
+
this.analyser.getByteFrequencyData(frequencies)
|
| 333 |
+
|
| 334 |
+
// Convert frequency to array index
|
| 335 |
+
const frequencyToIndex = (frequency: number): number => {
|
| 336 |
+
return Math.round(frequency / (this.audioContext!.sampleRate / 2) * this.analyser!.frequencyBinCount)
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
// Extract energy for each frequency range
|
| 340 |
+
const deepIndices = {
|
| 341 |
+
min: frequencyToIndex(this.frequencyMapping.deepFreq.min),
|
| 342 |
+
max: frequencyToIndex(this.frequencyMapping.deepFreq.max)
|
| 343 |
+
}
|
| 344 |
+
const midIndices = {
|
| 345 |
+
min: frequencyToIndex(this.frequencyMapping.midFreq.min),
|
| 346 |
+
max: frequencyToIndex(this.frequencyMapping.midFreq.max)
|
| 347 |
+
}
|
| 348 |
+
const highIndices = {
|
| 349 |
+
min: frequencyToIndex(this.frequencyMapping.highFreq.min),
|
| 350 |
+
max: frequencyToIndex(this.frequencyMapping.highFreq.max)
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
// Calculate normalized energy levels
|
| 354 |
+
const deepRange = frequencies.slice(deepIndices.min, deepIndices.max + 1)
|
| 355 |
+
const midRange = frequencies.slice(midIndices.min, midIndices.max + 1)
|
| 356 |
+
const highRange = frequencies.slice(highIndices.min, highIndices.max + 1)
|
| 357 |
+
|
| 358 |
+
const deepEnergy = deepRange.reduce((a, b) => a + b, 0) / deepRange.length / 255
|
| 359 |
+
const midEnergy = midRange.reduce((a, b) => a + b, 0) / midRange.length / 255
|
| 360 |
+
const highEnergy = highRange.reduce((a, b) => a + b, 0) / highRange.length / 255
|
| 361 |
+
|
| 362 |
+
// Overall amplitude
|
| 363 |
+
const overallAmplitude = frequencies.reduce((a, b) => a + b, 0) / frequencies.length / 255
|
| 364 |
+
|
| 365 |
+
// Peak detection
|
| 366 |
+
const currentEnergy = midEnergy * 255 // Use mid-range for peak detection
|
| 367 |
+
this.peakDetection.energyHistory.push(currentEnergy)
|
| 368 |
+
|
| 369 |
+
if (this.peakDetection.energyHistory.length > this.peakDetection.historyLength) {
|
| 370 |
+
this.peakDetection.energyHistory.shift()
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
const averageEnergy = this.peakDetection.energyHistory.reduce((a, b) => a + b, 0) /
|
| 374 |
+
this.peakDetection.energyHistory.length
|
| 375 |
+
|
| 376 |
+
const now = performance.now()
|
| 377 |
+
const peakDetected = currentEnergy > averageEnergy * this.peakDetection.sensitivity &&
|
| 378 |
+
now - this.peakDetection.lastPeakTime > this.peakDetection.minTimeBetweenPeaks
|
| 379 |
+
|
| 380 |
+
if (peakDetected) {
|
| 381 |
+
this.peakDetection.lastPeakTime = now
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
// Beat detection (based on deep frequencies)
|
| 385 |
+
const beatThreshold = 150 // Normalized threshold
|
| 386 |
+
const beatDetected = deepEnergy * 255 > beatThreshold
|
| 387 |
+
|
| 388 |
+
if (beatDetected && !this.beatManager.isWaveActive) {
|
| 389 |
+
this.beatManager.triggerWave(deepEnergy * 255)
|
| 390 |
+
}
|
| 391 |
+
|
| 392 |
+
return {
|
| 393 |
+
frequencies,
|
| 394 |
+
deepEnergy,
|
| 395 |
+
midEnergy,
|
| 396 |
+
highEnergy,
|
| 397 |
+
overallAmplitude,
|
| 398 |
+
peakDetected,
|
| 399 |
+
beatDetected
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
} catch (error) {
|
| 403 |
+
console.error('Audio analysis failed:', error)
|
| 404 |
+
return {
|
| 405 |
+
frequencies: new Uint8Array(1024),
|
| 406 |
+
deepEnergy: 0,
|
| 407 |
+
midEnergy: 0,
|
| 408 |
+
highEnergy: 0,
|
| 409 |
+
overallAmplitude: 0,
|
| 410 |
+
peakDetected: false,
|
| 411 |
+
beatDetected: false
|
| 412 |
+
}
|
| 413 |
+
}
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
// Public method to update beat manager
|
| 417 |
+
updateBeatManager(deltaTime: number): void {
|
| 418 |
+
this.beatManager.update(deltaTime)
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
// Public method to get wave force for visualization
|
| 422 |
+
getWaveForce(distance: number): number {
|
| 423 |
+
return this.beatManager.getWaveForce(distance)
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
+
cleanup(): void {
|
| 427 |
+
if (this.sourceNode) {
|
| 428 |
+
this.sourceNode.disconnect()
|
| 429 |
+
this.sourceNode = null
|
| 430 |
+
}
|
| 431 |
+
|
| 432 |
+
if (this.audioContext) {
|
| 433 |
+
this.audioContext.close()
|
| 434 |
+
this.audioContext = null
|
| 435 |
+
}
|
| 436 |
+
|
| 437 |
+
if (this.audioElement) {
|
| 438 |
+
this.audioElement.pause()
|
| 439 |
+
this.audioElement.src = ''
|
| 440 |
+
this.audioElement = null
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
this.analyser = null
|
| 444 |
+
this.isInitialized = false
|
| 445 |
+
|
| 446 |
+
console.log('AudioEngine cleaned up')
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
+
// New methods for better audio element integration
|
| 450 |
+
getCurrentTime(): number {
|
| 451 |
+
return this.audioElement?.currentTime || 0
|
| 452 |
+
}
|
| 453 |
+
|
| 454 |
+
getDuration(): number {
|
| 455 |
+
return this.audioElement?.duration || 0
|
| 456 |
+
}
|
| 457 |
+
|
| 458 |
+
getAudioElement(): HTMLAudioElement | null {
|
| 459 |
+
return this.audioElement
|
| 460 |
+
}
|
| 461 |
+
|
| 462 |
+
isPlaying(): boolean {
|
| 463 |
+
return this.audioElement ? !this.audioElement.paused : false
|
| 464 |
+
}
|
| 465 |
+
|
| 466 |
+
// Test method to verify basic audio functionality
|
| 467 |
+
async testAudioPlayback(url: string): Promise<boolean> {
|
| 468 |
+
console.log('🧪 Testing audio file accessibility and format...')
|
| 469 |
+
|
| 470 |
+
try {
|
| 471 |
+
// First test HTTP accessibility
|
| 472 |
+
const response = await fetch(url, { method: 'HEAD' })
|
| 473 |
+
if (!response.ok) {
|
| 474 |
+
console.error('❌ HTTP test failed:', response.status, response.statusText)
|
| 475 |
+
return false
|
| 476 |
+
}
|
| 477 |
+
|
| 478 |
+
const contentType = response.headers.get('content-type')
|
| 479 |
+
console.log('📊 Content-Type:', contentType)
|
| 480 |
+
|
| 481 |
+
if (!contentType || !contentType.includes('audio/')) {
|
| 482 |
+
console.error('❌ Invalid content type:', contentType)
|
| 483 |
+
return false
|
| 484 |
+
}
|
| 485 |
+
|
| 486 |
+
// Test audio element loading (without playing to avoid user gesture requirement)
|
| 487 |
+
const testAudio = document.createElement('audio')
|
| 488 |
+
testAudio.volume = 0.1 // Very low volume for safety
|
| 489 |
+
testAudio.src = url
|
| 490 |
+
|
| 491 |
+
return new Promise((resolve) => {
|
| 492 |
+
const timeout = setTimeout(() => {
|
| 493 |
+
cleanup()
|
| 494 |
+
console.error('❌ Audio test timeout')
|
| 495 |
+
resolve(false)
|
| 496 |
+
}, 5000)
|
| 497 |
+
|
| 498 |
+
const cleanup = () => {
|
| 499 |
+
clearTimeout(timeout)
|
| 500 |
+
testAudio.removeEventListener('loadedmetadata', onLoadedMetadata)
|
| 501 |
+
testAudio.removeEventListener('error', onError)
|
| 502 |
+
testAudio.src = ''
|
| 503 |
+
}
|
| 504 |
+
|
| 505 |
+
const onLoadedMetadata = () => {
|
| 506 |
+
console.log('✅ Audio metadata loaded successfully, duration:', testAudio.duration)
|
| 507 |
+
cleanup()
|
| 508 |
+
resolve(true)
|
| 509 |
+
}
|
| 510 |
+
|
| 511 |
+
const onError = (e: Event) => {
|
| 512 |
+
console.error('❌ Audio loading error:', e)
|
| 513 |
+
console.error('Error details:', testAudio.error)
|
| 514 |
+
cleanup()
|
| 515 |
+
resolve(false)
|
| 516 |
+
}
|
| 517 |
+
|
| 518 |
+
testAudio.addEventListener('loadedmetadata', onLoadedMetadata)
|
| 519 |
+
testAudio.addEventListener('error', onError)
|
| 520 |
+
testAudio.load()
|
| 521 |
+
})
|
| 522 |
+
} catch (error) {
|
| 523 |
+
console.error('❌ Audio test failed:', error)
|
| 524 |
+
return false
|
| 525 |
+
}
|
| 526 |
+
}
|
| 527 |
+
|
| 528 |
+
// Enhanced frequency analysis for particle systems
|
| 529 |
+
public getAdvancedAudioData(sphere: {
|
| 530 |
+
params: {
|
| 531 |
+
minFrequency: number
|
| 532 |
+
maxFrequency: number
|
| 533 |
+
minFrequencyBeat: number
|
| 534 |
+
maxFrequencyBeat: number
|
| 535 |
+
gainMultiplier: number
|
| 536 |
+
peakSensitivity: number
|
| 537 |
+
}
|
| 538 |
+
peakDetection: {
|
| 539 |
+
energyHistory: number[]
|
| 540 |
+
historyLength: number
|
| 541 |
+
lastPeakTime: number
|
| 542 |
+
minTimeBetweenPeaks: number
|
| 543 |
+
}
|
| 544 |
+
}): {
|
| 545 |
+
average: number
|
| 546 |
+
frequencies: Uint8Array
|
| 547 |
+
peakDetected: boolean
|
| 548 |
+
rangeEnergy: number
|
| 549 |
+
rangeEnergyBeat: number
|
| 550 |
+
} {
|
| 551 |
+
if (!this.analyser || !this.audioContext) {
|
| 552 |
+
return {
|
| 553 |
+
average: 0,
|
| 554 |
+
frequencies: new Uint8Array(),
|
| 555 |
+
peakDetected: false,
|
| 556 |
+
rangeEnergy: 0,
|
| 557 |
+
rangeEnergyBeat: 0
|
| 558 |
+
}
|
| 559 |
+
}
|
| 560 |
+
|
| 561 |
+
try {
|
| 562 |
+
const frequencies = new Uint8Array(this.analyser.frequencyBinCount)
|
| 563 |
+
this.analyser.getByteFrequencyData(frequencies)
|
| 564 |
+
|
| 565 |
+
// Apply gain multiplier
|
| 566 |
+
const gainMultiplier = sphere.params.gainMultiplier
|
| 567 |
+
frequencies.forEach((value, index) => {
|
| 568 |
+
frequencies[index] = Math.min(value * gainMultiplier, 255)
|
| 569 |
+
})
|
| 570 |
+
|
| 571 |
+
// Calculate frequency range indices
|
| 572 |
+
const frequencyToIndex = (frequency: number) =>
|
| 573 |
+
Math.round(frequency / (this.audioContext!.sampleRate / 2) * this.analyser!.frequencyBinCount)
|
| 574 |
+
|
| 575 |
+
// Main frequency range for visualization
|
| 576 |
+
const minFreqIndex = frequencyToIndex(sphere.params.minFrequency)
|
| 577 |
+
const maxFreqIndex = frequencyToIndex(sphere.params.maxFrequency)
|
| 578 |
+
const frequencyRange = frequencies.slice(minFreqIndex, maxFreqIndex + 1)
|
| 579 |
+
const rangeEnergy = frequencyRange.reduce((a, b) => a + b, 0) / frequencyRange.length
|
| 580 |
+
|
| 581 |
+
// Beat detection frequency range
|
| 582 |
+
const minFreqBeatIndex = frequencyToIndex(sphere.params.minFrequencyBeat)
|
| 583 |
+
const maxFreqBeatIndex = frequencyToIndex(sphere.params.maxFrequencyBeat)
|
| 584 |
+
const frequencyRangeBeat = frequencies.slice(minFreqBeatIndex, maxFreqBeatIndex + 1)
|
| 585 |
+
const rangeEnergyBeat = frequencyRangeBeat.reduce((a, b) => a + b, 0) / frequencyRangeBeat.length
|
| 586 |
+
|
| 587 |
+
// Peak detection logic
|
| 588 |
+
sphere.peakDetection.energyHistory.push(rangeEnergy)
|
| 589 |
+
if (sphere.peakDetection.energyHistory.length > sphere.peakDetection.historyLength) {
|
| 590 |
+
sphere.peakDetection.energyHistory.shift()
|
| 591 |
+
}
|
| 592 |
+
|
| 593 |
+
const averageEnergy = sphere.peakDetection.energyHistory.reduce((a, b) => a + b, 0) /
|
| 594 |
+
sphere.peakDetection.energyHistory.length
|
| 595 |
+
|
| 596 |
+
const now = performance.now()
|
| 597 |
+
const peakDetected = rangeEnergy > averageEnergy * sphere.params.peakSensitivity &&
|
| 598 |
+
now - sphere.peakDetection.lastPeakTime > sphere.peakDetection.minTimeBetweenPeaks
|
| 599 |
+
|
| 600 |
+
if (peakDetected) {
|
| 601 |
+
sphere.peakDetection.lastPeakTime = now
|
| 602 |
+
console.log(`🎵 PEAK DETECTED! Energy: ${rangeEnergy.toFixed(1)}, Average: ${averageEnergy.toFixed(1)}`)
|
| 603 |
+
}
|
| 604 |
+
|
| 605 |
+
return {
|
| 606 |
+
average: rangeEnergy / 255,
|
| 607 |
+
frequencies,
|
| 608 |
+
peakDetected,
|
| 609 |
+
rangeEnergy,
|
| 610 |
+
rangeEnergyBeat
|
| 611 |
+
}
|
| 612 |
+
|
| 613 |
+
} catch (error) {
|
| 614 |
+
console.error('❌ Enhanced audio analysis failed:', error)
|
| 615 |
+
return {
|
| 616 |
+
average: 0,
|
| 617 |
+
frequencies: new Uint8Array(),
|
| 618 |
+
peakDetected: false,
|
| 619 |
+
rangeEnergy: 0,
|
| 620 |
+
rangeEnergyBeat: 0
|
| 621 |
+
}
|
| 622 |
+
}
|
| 623 |
+
}
|
| 624 |
+
|
| 625 |
+
// Get smooth volume for rotation effects
|
| 626 |
+
public getSmoothVolume(lastValidVolume: number, volumeChangeThreshold: number): {
|
| 627 |
+
volume: number
|
| 628 |
+
shouldUpdate: boolean
|
| 629 |
+
} {
|
| 630 |
+
if (!this.analyser) {
|
| 631 |
+
return { volume: 0, shouldUpdate: false }
|
| 632 |
+
}
|
| 633 |
+
|
| 634 |
+
const bufferLength = this.analyser.frequencyBinCount
|
| 635 |
+
const dataArray = new Uint8Array(bufferLength)
|
| 636 |
+
this.analyser.getByteFrequencyData(dataArray)
|
| 637 |
+
|
| 638 |
+
let sum = 0
|
| 639 |
+
for (let i = 0; i < bufferLength; i++) {
|
| 640 |
+
sum += dataArray[i]
|
| 641 |
+
}
|
| 642 |
+
const average = sum / bufferLength
|
| 643 |
+
const normalizedVolume = average / 255
|
| 644 |
+
|
| 645 |
+
let shouldUpdate = true
|
| 646 |
+
if (lastValidVolume === 0) {
|
| 647 |
+
lastValidVolume = normalizedVolume
|
| 648 |
+
} else {
|
| 649 |
+
const change = Math.abs(normalizedVolume - lastValidVolume)
|
| 650 |
+
if (change <= volumeChangeThreshold) {
|
| 651 |
+
lastValidVolume = normalizedVolume
|
| 652 |
+
} else {
|
| 653 |
+
shouldUpdate = false
|
| 654 |
+
}
|
| 655 |
+
}
|
| 656 |
+
|
| 657 |
+
return { volume: lastValidVolume, shouldUpdate }
|
| 658 |
+
}
|
| 659 |
+
|
| 660 |
+
// Auto-play functionality
|
| 661 |
+
public setOnTrackEndedCallback(callback: (() => void) | null): void {
|
| 662 |
+
this.onTrackEndedCallback = callback
|
| 663 |
+
console.log('🎵 Auto-play callback', callback ? 'enabled' : 'disabled')
|
| 664 |
+
}
|
| 665 |
+
}
|
src/main.tsx
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import ReactDOM from 'react-dom/client'
|
| 2 |
+
import LivingNexusApp from './components/LivingNexusApp'
|
| 3 |
+
import './styles/global.css'
|
| 4 |
+
|
| 5 |
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
| 6 |
+
<LivingNexusApp />
|
| 7 |
+
)
|
src/store/nexusStore.ts
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { create } from 'zustand'
|
| 2 |
+
import type { AudioData, PlaybackState, Track } from '../types/audio'
|
| 3 |
+
import type { InteractionState, VisualizationParams } from '../types/visualization'
|
| 4 |
+
|
| 5 |
+
interface NexusStore {
|
| 6 |
+
// Audio State
|
| 7 |
+
audioData: AudioData
|
| 8 |
+
playback: PlaybackState
|
| 9 |
+
isAudioInitialized: boolean
|
| 10 |
+
autoPlayEnabled: boolean
|
| 11 |
+
|
| 12 |
+
// Visualization State
|
| 13 |
+
visualParams: VisualizationParams
|
| 14 |
+
interactionState: InteractionState
|
| 15 |
+
|
| 16 |
+
// Social Links
|
| 17 |
+
socialLinks: Array<{
|
| 18 |
+
id: string
|
| 19 |
+
platform: string
|
| 20 |
+
url: string
|
| 21 |
+
glyphPath: string
|
| 22 |
+
}>
|
| 23 |
+
|
| 24 |
+
// Actions
|
| 25 |
+
updateAudioData: (data: AudioData) => void
|
| 26 |
+
setPlaybackState: (state: Partial<PlaybackState>) => void
|
| 27 |
+
setCurrentTrack: (track: Track | null) => void
|
| 28 |
+
setIsPlaying: (playing: boolean) => void
|
| 29 |
+
setVolume: (volume: number) => void
|
| 30 |
+
setCurrentTime: (time: number) => void
|
| 31 |
+
updateVisualizationParams: (params: Partial<VisualizationParams>) => void
|
| 32 |
+
setInteractionState: (state: Partial<InteractionState>) => void
|
| 33 |
+
initializeAudio: () => void
|
| 34 |
+
setAutoPlayEnabled: (enabled: boolean) => void
|
| 35 |
+
getNextTrack: () => Track | null
|
| 36 |
+
getPreviousTrack: () => Track | null
|
| 37 |
+
getCurrentTrackIndex: () => number
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
const defaultAudioData: AudioData = {
|
| 41 |
+
frequencies: new Uint8Array(1024),
|
| 42 |
+
deepEnergy: 0,
|
| 43 |
+
midEnergy: 0,
|
| 44 |
+
highEnergy: 0,
|
| 45 |
+
overallAmplitude: 0,
|
| 46 |
+
peakDetected: false,
|
| 47 |
+
beatDetected: false
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
const defaultPlaybackState: PlaybackState = {
|
| 51 |
+
isPlaying: false,
|
| 52 |
+
currentTrack: null,
|
| 53 |
+
currentTime: 0,
|
| 54 |
+
volume: 0.7,
|
| 55 |
+
tracks: []
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
const defaultVisualizationParams: VisualizationParams = {
|
| 59 |
+
worldBreathRate: 0.8,
|
| 60 |
+
deepPulseIntensity: 1.0,
|
| 61 |
+
midGlowIntensity: 1.0,
|
| 62 |
+
highShimmerCount: 50,
|
| 63 |
+
noiseScale: 4.0,
|
| 64 |
+
colorPalette: {
|
| 65 |
+
primary: '#2D1B0E', // Deep earth brown
|
| 66 |
+
secondary: '#8B6914', // Burnished bronze
|
| 67 |
+
accent: '#FFD700', // Mystical gold
|
| 68 |
+
ember: '#FF6B35' // Ember orange
|
| 69 |
+
}
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
const defaultInteractionState: InteractionState = {
|
| 73 |
+
isDormant: true,
|
| 74 |
+
isAwakening: false,
|
| 75 |
+
isAlive: false,
|
| 76 |
+
lastInteraction: 0
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
export const useNexusStore = create<NexusStore>((set, get) => ({
|
| 80 |
+
// Initial State
|
| 81 |
+
audioData: defaultAudioData,
|
| 82 |
+
playback: defaultPlaybackState,
|
| 83 |
+
isAudioInitialized: false,
|
| 84 |
+
autoPlayEnabled: true, // Enable auto-play by default
|
| 85 |
+
visualParams: defaultVisualizationParams,
|
| 86 |
+
interactionState: defaultInteractionState,
|
| 87 |
+
|
| 88 |
+
socialLinks: [
|
| 89 |
+
{
|
| 90 |
+
id: 'spotify',
|
| 91 |
+
platform: 'Spotify',
|
| 92 |
+
url: 'https://open.spotify.com/artist/your-band',
|
| 93 |
+
glyphPath: 'M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2z'
|
| 94 |
+
},
|
| 95 |
+
{
|
| 96 |
+
id: 'apple',
|
| 97 |
+
platform: 'Apple Music',
|
| 98 |
+
url: 'https://music.apple.com/artist/your-band',
|
| 99 |
+
glyphPath: 'M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2z'
|
| 100 |
+
},
|
| 101 |
+
{
|
| 102 |
+
id: 'instagram',
|
| 103 |
+
platform: 'Instagram',
|
| 104 |
+
url: 'https://instagram.com/your-band',
|
| 105 |
+
glyphPath: 'M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2z'
|
| 106 |
+
}
|
| 107 |
+
],
|
| 108 |
+
|
| 109 |
+
// Actions
|
| 110 |
+
updateAudioData: (data: AudioData) => set({ audioData: data }),
|
| 111 |
+
|
| 112 |
+
setPlaybackState: (state: Partial<PlaybackState>) =>
|
| 113 |
+
set(prev => ({ playback: { ...prev.playback, ...state } })),
|
| 114 |
+
|
| 115 |
+
setCurrentTrack: (track: Track | null) =>
|
| 116 |
+
set(prev => ({ playback: { ...prev.playback, currentTrack: track } })),
|
| 117 |
+
|
| 118 |
+
setIsPlaying: (playing: boolean) => {
|
| 119 |
+
set(prev => ({ playback: { ...prev.playback, isPlaying: playing } }))
|
| 120 |
+
|
| 121 |
+
// Trigger interaction state changes
|
| 122 |
+
const now = Date.now()
|
| 123 |
+
if (playing) {
|
| 124 |
+
set(prev => ({
|
| 125 |
+
interactionState: {
|
| 126 |
+
...prev.interactionState,
|
| 127 |
+
isDormant: false,
|
| 128 |
+
isAwakening: true,
|
| 129 |
+
lastInteraction: now
|
| 130 |
+
}
|
| 131 |
+
}))
|
| 132 |
+
|
| 133 |
+
// After awakening animation, set to alive
|
| 134 |
+
setTimeout(() => {
|
| 135 |
+
set(prev => ({
|
| 136 |
+
interactionState: {
|
| 137 |
+
...prev.interactionState,
|
| 138 |
+
isAwakening: false,
|
| 139 |
+
isAlive: true
|
| 140 |
+
}
|
| 141 |
+
}))
|
| 142 |
+
}, 2000) // 2 second awakening sequence
|
| 143 |
+
} else {
|
| 144 |
+
set(prev => ({
|
| 145 |
+
interactionState: {
|
| 146 |
+
...prev.interactionState,
|
| 147 |
+
isAlive: false,
|
| 148 |
+
isDormant: true,
|
| 149 |
+
isAwakening: false
|
| 150 |
+
}
|
| 151 |
+
}))
|
| 152 |
+
}
|
| 153 |
+
},
|
| 154 |
+
|
| 155 |
+
setVolume: (volume: number) =>
|
| 156 |
+
set(prev => ({ playback: { ...prev.playback, volume } })),
|
| 157 |
+
|
| 158 |
+
setCurrentTime: (time: number) =>
|
| 159 |
+
set(prev => ({ playback: { ...prev.playback, currentTime: time } })),
|
| 160 |
+
|
| 161 |
+
updateVisualizationParams: (params: Partial<VisualizationParams>) =>
|
| 162 |
+
set(prev => ({ visualParams: { ...prev.visualParams, ...params } })),
|
| 163 |
+
|
| 164 |
+
setInteractionState: (state: Partial<InteractionState>) =>
|
| 165 |
+
set(prev => ({ interactionState: { ...prev.interactionState, ...state } })),
|
| 166 |
+
|
| 167 |
+
initializeAudio: () => set({ isAudioInitialized: true }),
|
| 168 |
+
|
| 169 |
+
setAutoPlayEnabled: (enabled: boolean) => set({ autoPlayEnabled: enabled }),
|
| 170 |
+
|
| 171 |
+
getCurrentTrackIndex: (): number => {
|
| 172 |
+
const { playback } = get()
|
| 173 |
+
if (!playback.currentTrack || playback.tracks.length === 0) return -1
|
| 174 |
+
return playback.tracks.findIndex(track => track.id === playback.currentTrack?.id)
|
| 175 |
+
},
|
| 176 |
+
|
| 177 |
+
getNextTrack: (): Track | null => {
|
| 178 |
+
const { playback } = get()
|
| 179 |
+
const currentIndex = get().getCurrentTrackIndex()
|
| 180 |
+
if (currentIndex === -1 || currentIndex >= playback.tracks.length - 1) return null
|
| 181 |
+
return playback.tracks[currentIndex + 1]
|
| 182 |
+
},
|
| 183 |
+
|
| 184 |
+
getPreviousTrack: (): Track | null => {
|
| 185 |
+
const { playback } = get()
|
| 186 |
+
const currentIndex = get().getCurrentTrackIndex()
|
| 187 |
+
if (currentIndex <= 0) return null
|
| 188 |
+
return playback.tracks[currentIndex - 1]
|
| 189 |
+
}
|
| 190 |
+
}))
|
src/styles/global.css
ADDED
|
@@ -0,0 +1,965 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Typography - Ancient Transmission Scripts */
|
| 2 |
+
@import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600;700&family=Uncial+Antiqua&family=Metamorphous&display=swap');
|
| 3 |
+
|
| 4 |
+
/* Living Nexus Global Styles - Ancient Botanical Interface */
|
| 5 |
+
|
| 6 |
+
* {
|
| 7 |
+
margin: 0;
|
| 8 |
+
padding: 0;
|
| 9 |
+
box-sizing: border-box;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
html, body {
|
| 13 |
+
height: 100vh;
|
| 14 |
+
width: 100vw;
|
| 15 |
+
overflow: hidden;
|
| 16 |
+
font-family: 'Metamorphous', 'Uncial Antiqua', serif;
|
| 17 |
+
background: #0F1B13; /* Deep forest void */
|
| 18 |
+
color: #A67B5B; /* Earthy copper text */
|
| 19 |
+
|
| 20 |
+
/* Subtle organic texture */
|
| 21 |
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4'%3E%3Cpath d='M0,0h1v1h-1zM2,2h1v1h-1z' fill='%232D5016' opacity='0.03'/%3E%3C/svg%3E");
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
#root {
|
| 25 |
+
height: 100vh;
|
| 26 |
+
width: 100vw;
|
| 27 |
+
position: relative;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
/* Main Living Nexus Artifact Container */
|
| 31 |
+
.living-nexus-artifact {
|
| 32 |
+
position: relative;
|
| 33 |
+
width: 100vw;
|
| 34 |
+
height: 100vh;
|
| 35 |
+
overflow: hidden;
|
| 36 |
+
background: #0F1B13;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
/* Main Visualization Container - Full viewport background */
|
| 40 |
+
.visualization-container {
|
| 41 |
+
position: fixed;
|
| 42 |
+
top: 0;
|
| 43 |
+
left: 0;
|
| 44 |
+
width: 100vw;
|
| 45 |
+
height: 100vh;
|
| 46 |
+
z-index: 1;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
/* Audio Player Container - Fixed as left vertical dock */
|
| 50 |
+
.audio-player-container {
|
| 51 |
+
position: fixed;
|
| 52 |
+
top: 0;
|
| 53 |
+
left: 0;
|
| 54 |
+
bottom: 0;
|
| 55 |
+
z-index: 1000;
|
| 56 |
+
pointer-events: none;
|
| 57 |
+
width: 350px; /* Fixed width for the dock */
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
.audio-player-container .control-plinth {
|
| 61 |
+
pointer-events: all;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
/* Interaction States for the Artifact */
|
| 65 |
+
.living-nexus-artifact.dormant {
|
| 66 |
+
opacity: 0.8;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
.living-nexus-artifact.awakening {
|
| 70 |
+
animation: awakeningSequence 2s ease-in-out;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
.living-nexus-artifact.alive {
|
| 74 |
+
opacity: 1;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
/* Ancient Botanical & Celestial Color Palette */
|
| 78 |
+
:root {
|
| 79 |
+
/* Deep Forest & Earth Tones */
|
| 80 |
+
--forest-deep: #0F1B13; /* Deep forest background */
|
| 81 |
+
--forest-medium: #1C2E1F; /* Medium forest */
|
| 82 |
+
--forest-light: #2D5016; /* Light forest green */
|
| 83 |
+
--moss-dark: #3A4D2A; /* Dark moss */
|
| 84 |
+
--moss-medium: #4F6B3E; /* Medium moss */
|
| 85 |
+
--moss-light: #6B8C42; /* Light moss */
|
| 86 |
+
|
| 87 |
+
/* Earthy Copper & Bronze */
|
| 88 |
+
--copper-dark: #5D4037; /* Deep earth copper */
|
| 89 |
+
--copper-medium: #795548; /* Medium copper */
|
| 90 |
+
--copper-light: #A67B5B; /* Light copper text */
|
| 91 |
+
--bronze-warm: #8D6E63; /* Warm bronze */
|
| 92 |
+
|
| 93 |
+
/* Celestial Nebula Accents */
|
| 94 |
+
--nebula-deep: #2A1810; /* Deep space brown */
|
| 95 |
+
--nebula-purple: #4A306B; /* Celestial purple */
|
| 96 |
+
--nebula-teal: #3E5B4C; /* Nebula teal */
|
| 97 |
+
--stellar-copper: #B8860B; /* Stellar copper highlights */
|
| 98 |
+
|
| 99 |
+
/* Organic Textures */
|
| 100 |
+
--bark-brown: #3E2723; /* Tree bark */
|
| 101 |
+
--root-brown: #4E342E; /* Root brown */
|
| 102 |
+
--earth-dark: #1B0E0A; /* Deep earth */
|
| 103 |
+
|
| 104 |
+
/* Gradients */
|
| 105 |
+
--forest-gradient: linear-gradient(
|
| 106 |
+
135deg,
|
| 107 |
+
var(--forest-light) 0%,
|
| 108 |
+
var(--forest-medium) 50%,
|
| 109 |
+
var(--forest-deep) 100%
|
| 110 |
+
);
|
| 111 |
+
--copper-gradient: linear-gradient(
|
| 112 |
+
135deg,
|
| 113 |
+
var(--stellar-copper) 0%,
|
| 114 |
+
var(--bronze-warm) 50%,
|
| 115 |
+
var(--copper-dark) 100%
|
| 116 |
+
);
|
| 117 |
+
--nebula-gradient: linear-gradient(
|
| 118 |
+
45deg,
|
| 119 |
+
var(--nebula-purple) 0%,
|
| 120 |
+
var(--nebula-teal) 50%,
|
| 121 |
+
var(--forest-medium) 100%
|
| 122 |
+
);
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
/* Organic Energy Matrix */
|
| 126 |
+
body::before {
|
| 127 |
+
content: '';
|
| 128 |
+
position: fixed;
|
| 129 |
+
top: 0;
|
| 130 |
+
left: 0;
|
| 131 |
+
width: 100%;
|
| 132 |
+
height: 100%;
|
| 133 |
+
background:
|
| 134 |
+
radial-gradient(circle at 20% 80%, rgba(77, 107, 62, 0.06) 0%, transparent 60%),
|
| 135 |
+
radial-gradient(circle at 80% 20%, rgba(74, 48, 107, 0.04) 0%, transparent 60%),
|
| 136 |
+
radial-gradient(circle at 40% 40%, rgba(166, 123, 91, 0.03) 0%, transparent 70%),
|
| 137 |
+
radial-gradient(circle at 60% 60%, rgba(62, 91, 76, 0.02) 0%, transparent 80%);
|
| 138 |
+
background-size: 900px 900px, 700px 700px, 1100px 1100px, 500px 500px;
|
| 139 |
+
animation: organicPulse 20s ease-in-out infinite;
|
| 140 |
+
pointer-events: none;
|
| 141 |
+
z-index: -1;
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
@keyframes organicPulse {
|
| 145 |
+
0%, 100% {
|
| 146 |
+
opacity: 0.15;
|
| 147 |
+
transform: scale(1) rotate(0deg);
|
| 148 |
+
}
|
| 149 |
+
25% {
|
| 150 |
+
opacity: 0.25;
|
| 151 |
+
transform: scale(1.01) rotate(0.2deg);
|
| 152 |
+
}
|
| 153 |
+
50% {
|
| 154 |
+
opacity: 0.35;
|
| 155 |
+
transform: scale(1.02) rotate(0deg);
|
| 156 |
+
}
|
| 157 |
+
75% {
|
| 158 |
+
opacity: 0.2;
|
| 159 |
+
transform: scale(1.01) rotate(-0.2deg);
|
| 160 |
+
}
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
h1, h2, h3 {
|
| 164 |
+
font-family: 'Cinzel', serif;
|
| 165 |
+
font-weight: 700;
|
| 166 |
+
letter-spacing: 3px;
|
| 167 |
+
text-transform: uppercase;
|
| 168 |
+
color: var(--copper-light);
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
/* Runic Text Classes */
|
| 172 |
+
.runic-text {
|
| 173 |
+
font-family: 'Uncial Antiqua', serif;
|
| 174 |
+
letter-spacing: 0.15em;
|
| 175 |
+
text-transform: lowercase;
|
| 176 |
+
color: var(--bronze-warm);
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
.transmission-text {
|
| 180 |
+
font-family: 'Metamorphous', serif;
|
| 181 |
+
color: var(--moss-medium);
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
.ancient-title {
|
| 185 |
+
font-family: 'Cinzel', serif;
|
| 186 |
+
font-weight: 700;
|
| 187 |
+
text-transform: uppercase;
|
| 188 |
+
letter-spacing: 0.3em;
|
| 189 |
+
color: var(--stellar-copper);
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
/* Runic Glyph Animation Base */
|
| 193 |
+
.glyph, .rune {
|
| 194 |
+
display: inline-block;
|
| 195 |
+
color: var(--stellar-copper);
|
| 196 |
+
transition: all 0.4s ease;
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
.glyph:hover, .rune:hover {
|
| 200 |
+
transform: scale(1.15) rotate(5deg);
|
| 201 |
+
color: var(--copper-light);
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
/* Living Tree Styles */
|
| 205 |
+
.world-tree {
|
| 206 |
+
position: absolute;
|
| 207 |
+
top: 0;
|
| 208 |
+
left: 0;
|
| 209 |
+
width: 100vw;
|
| 210 |
+
height: 100vh;
|
| 211 |
+
z-index: 1;
|
| 212 |
+
pointer-events: none; /* Allow clicks to pass through to controls */
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
.living-nexus-artifact .world-tree {
|
| 216 |
+
position: absolute;
|
| 217 |
+
top: 0;
|
| 218 |
+
left: 0;
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
.tree-trunk {
|
| 222 |
+
stroke: var(--bark-brown);
|
| 223 |
+
stroke-width: 8;
|
| 224 |
+
animation: trunkPulse 3s ease-in-out infinite;
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
@keyframes trunkPulse {
|
| 228 |
+
0%, 100% {
|
| 229 |
+
stroke-width: 8;
|
| 230 |
+
opacity: 0.8;
|
| 231 |
+
}
|
| 232 |
+
50% {
|
| 233 |
+
stroke-width: 12;
|
| 234 |
+
opacity: 1;
|
| 235 |
+
}
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
.tree-branches {
|
| 239 |
+
stroke: var(--bronze-warm);
|
| 240 |
+
stroke-width: 2;
|
| 241 |
+
opacity: 0.7;
|
| 242 |
+
animation: branchShimmer 2s ease-in-out infinite;
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
@keyframes branchShimmer {
|
| 246 |
+
0%, 100% { opacity: 0.6; }
|
| 247 |
+
50% { opacity: 0.9; }
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
.tree-roots {
|
| 251 |
+
stroke: var(--root-brown);
|
| 252 |
+
stroke-width: 6;
|
| 253 |
+
opacity: 0.8;
|
| 254 |
+
animation: rootGlow 4s ease-in-out infinite;
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
@keyframes rootGlow {
|
| 258 |
+
0%, 100% {
|
| 259 |
+
opacity: 0.8;
|
| 260 |
+
}
|
| 261 |
+
50% {
|
| 262 |
+
opacity: 1;
|
| 263 |
+
}
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
/* Core Glyphs */
|
| 267 |
+
.core-glyph {
|
| 268 |
+
fill: var(--stellar-copper);
|
| 269 |
+
stroke: var(--bronze-warm);
|
| 270 |
+
stroke-width: 1;
|
| 271 |
+
animation: glyphPulse 2.5s ease-in-out infinite;
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
@keyframes glyphPulse {
|
| 275 |
+
0%, 100% {
|
| 276 |
+
transform: scale(1);
|
| 277 |
+
opacity: 0.8;
|
| 278 |
+
}
|
| 279 |
+
50% {
|
| 280 |
+
transform: scale(1.1);
|
| 281 |
+
opacity: 1;
|
| 282 |
+
}
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
/* Ember Particles */
|
| 286 |
+
.ember-particle {
|
| 287 |
+
fill: var(--moss-light);
|
| 288 |
+
opacity: 0.7;
|
| 289 |
+
animation: emberFloat 3s ease-in-out infinite;
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
@keyframes emberFloat {
|
| 293 |
+
0% {
|
| 294 |
+
transform: translateY(0px);
|
| 295 |
+
opacity: 0.7;
|
| 296 |
+
}
|
| 297 |
+
50% {
|
| 298 |
+
transform: translateY(-10px);
|
| 299 |
+
opacity: 1;
|
| 300 |
+
}
|
| 301 |
+
100% {
|
| 302 |
+
transform: translateY(0px);
|
| 303 |
+
opacity: 0.7;
|
| 304 |
+
}
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
/* Energy Flow Paths */
|
| 308 |
+
.energy-path {
|
| 309 |
+
stroke: var(--stellar-copper);
|
| 310 |
+
stroke-width: 2;
|
| 311 |
+
fill: none;
|
| 312 |
+
opacity: 0.4;
|
| 313 |
+
stroke-dasharray: 5 5;
|
| 314 |
+
animation: energyFlow 4s linear infinite;
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
/* Control Plinth Component Styles - Updated for Dock */
|
| 318 |
+
.control-plinth {
|
| 319 |
+
display: flex;
|
| 320 |
+
flex-direction: column;
|
| 321 |
+
align-items: center;
|
| 322 |
+
gap: 15px;
|
| 323 |
+
min-width: 600px;
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
/* Vertical Dock Specific Styles */
|
| 327 |
+
.control-plinth-dock {
|
| 328 |
+
font-family: 'Cinzel', serif;
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
.control-plinth-dock .central-glyph {
|
| 332 |
+
background: var(--copper-gradient);
|
| 333 |
+
border: 2px solid var(--stellar-copper);
|
| 334 |
+
cursor: pointer;
|
| 335 |
+
display: flex;
|
| 336 |
+
align-items: center;
|
| 337 |
+
justify-content: center;
|
| 338 |
+
color: var(--forest-deep);
|
| 339 |
+
transition: all 0.3s ease;
|
| 340 |
+
border-radius: 50%;
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
.control-plinth-dock .central-glyph:hover {
|
| 344 |
+
transform: scale(1.1);
|
| 345 |
+
background: var(--nebula-gradient);
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
.control-plinth-dock .plinth-control {
|
| 349 |
+
background: transparent;
|
| 350 |
+
border: 1px solid var(--bronze-warm);
|
| 351 |
+
color: var(--copper-light);
|
| 352 |
+
border-radius: 6px;
|
| 353 |
+
font-family: 'Cinzel', serif;
|
| 354 |
+
cursor: pointer;
|
| 355 |
+
transition: all 0.3s ease;
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
.control-plinth-dock .plinth-control:hover {
|
| 359 |
+
background: var(--bronze-warm);
|
| 360 |
+
color: var(--forest-deep);
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
.control-plinth-dock .timeline-channel {
|
| 364 |
+
background: var(--bark-brown);
|
| 365 |
+
border-radius: 4px;
|
| 366 |
+
position: relative;
|
| 367 |
+
border: 1px solid var(--bronze-warm);
|
| 368 |
+
overflow: hidden;
|
| 369 |
+
}
|
| 370 |
+
|
| 371 |
+
.control-plinth-dock .timeline-indicator {
|
| 372 |
+
height: 100%;
|
| 373 |
+
background: var(--copper-gradient);
|
| 374 |
+
border-radius: 4px;
|
| 375 |
+
transition: width 0.1s ease;
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
.control-plinth-dock .volume-slider {
|
| 379 |
+
height: 4px;
|
| 380 |
+
background: var(--bark-brown);
|
| 381 |
+
border-radius: 2px;
|
| 382 |
+
outline: none;
|
| 383 |
+
cursor: pointer;
|
| 384 |
+
-webkit-appearance: none;
|
| 385 |
+
}
|
| 386 |
+
|
| 387 |
+
.control-plinth-dock .volume-slider::-webkit-slider-thumb {
|
| 388 |
+
-webkit-appearance: none;
|
| 389 |
+
width: 12px;
|
| 390 |
+
height: 12px;
|
| 391 |
+
border-radius: 50%;
|
| 392 |
+
background: var(--stellar-copper);
|
| 393 |
+
cursor: pointer;
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
/* Enhanced Dock Animations */
|
| 397 |
+
@keyframes glyphPulse {
|
| 398 |
+
0%, 100% {
|
| 399 |
+
transform: scale(1);
|
| 400 |
+
}
|
| 401 |
+
50% {
|
| 402 |
+
transform: scale(1.05);
|
| 403 |
+
}
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
/* Collapsed Dock Scrollbar Styling */
|
| 407 |
+
.control-plinth-dock .track-list::-webkit-scrollbar {
|
| 408 |
+
width: 6px;
|
| 409 |
+
}
|
| 410 |
+
|
| 411 |
+
.control-plinth-dock .track-list::-webkit-scrollbar-track {
|
| 412 |
+
background: rgba(166, 123, 91, 0.1);
|
| 413 |
+
border-radius: 3px;
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
.control-plinth-dock .track-list::-webkit-scrollbar-thumb {
|
| 417 |
+
background: rgba(166, 123, 91, 0.4);
|
| 418 |
+
border-radius: 3px;
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
.control-plinth-dock .track-list::-webkit-scrollbar-thumb:hover {
|
| 422 |
+
background: rgba(166, 123, 91, 0.6);
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
.track-info {
|
| 426 |
+
text-align: center;
|
| 427 |
+
margin-bottom: 10px;
|
| 428 |
+
}
|
| 429 |
+
|
| 430 |
+
.track-title {
|
| 431 |
+
font-size: 18px;
|
| 432 |
+
font-weight: 600;
|
| 433 |
+
color: var(--stellar-copper);
|
| 434 |
+
margin-bottom: 4px;
|
| 435 |
+
}
|
| 436 |
+
|
| 437 |
+
.track-artist {
|
| 438 |
+
font-size: 14px;
|
| 439 |
+
color: var(--bronze-warm);
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
.plinth-controls {
|
| 443 |
+
display: flex;
|
| 444 |
+
align-items: center;
|
| 445 |
+
gap: 15px;
|
| 446 |
+
width: 100%;
|
| 447 |
+
justify-content: center;
|
| 448 |
+
}
|
| 449 |
+
|
| 450 |
+
.volume-control {
|
| 451 |
+
display: flex;
|
| 452 |
+
align-items: center;
|
| 453 |
+
gap: 8px;
|
| 454 |
+
}
|
| 455 |
+
|
| 456 |
+
.volume-slider {
|
| 457 |
+
width: 80px;
|
| 458 |
+
height: 4px;
|
| 459 |
+
background: var(--bark-brown);
|
| 460 |
+
border-radius: 2px;
|
| 461 |
+
outline: none;
|
| 462 |
+
cursor: pointer;
|
| 463 |
+
}
|
| 464 |
+
|
| 465 |
+
.volume-slider::-webkit-slider-thumb {
|
| 466 |
+
appearance: none;
|
| 467 |
+
width: 12px;
|
| 468 |
+
height: 12px;
|
| 469 |
+
border-radius: 50%;
|
| 470 |
+
background: var(--stellar-copper);
|
| 471 |
+
cursor: pointer;
|
| 472 |
+
}
|
| 473 |
+
|
| 474 |
+
.time-display {
|
| 475 |
+
display: flex;
|
| 476 |
+
gap: 8px;
|
| 477 |
+
font-size: 12px;
|
| 478 |
+
color: var(--bronze-warm);
|
| 479 |
+
font-family: 'Cinzel', serif;
|
| 480 |
+
}
|
| 481 |
+
|
| 482 |
+
.track-list-tablet {
|
| 483 |
+
background: rgba(0, 0, 0, 0.8);
|
| 484 |
+
border: 1px solid rgba(255, 215, 0, 0.2);
|
| 485 |
+
border-radius: 6px;
|
| 486 |
+
padding: 15px;
|
| 487 |
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.6);
|
| 488 |
+
max-height: 200px;
|
| 489 |
+
overflow-y: auto;
|
| 490 |
+
min-width: 350px;
|
| 491 |
+
margin-top: 10px;
|
| 492 |
+
backdrop-filter: blur(10px);
|
| 493 |
+
}
|
| 494 |
+
|
| 495 |
+
.tablet-header {
|
| 496 |
+
display: flex;
|
| 497 |
+
justify-content: center;
|
| 498 |
+
align-items: center;
|
| 499 |
+
margin-bottom: 10px;
|
| 500 |
+
padding-bottom: 8px;
|
| 501 |
+
border-bottom: 1px solid rgba(255, 215, 0, 0.3);
|
| 502 |
+
}
|
| 503 |
+
|
| 504 |
+
.tablet-header h3 {
|
| 505 |
+
margin: 0;
|
| 506 |
+
color: #FFD700;
|
| 507 |
+
font-size: 14px;
|
| 508 |
+
}
|
| 509 |
+
|
| 510 |
+
.close-tablet {
|
| 511 |
+
background: none;
|
| 512 |
+
border: 1px solid var(--burnished-bronze);
|
| 513 |
+
color: var(--burnished-bronze);
|
| 514 |
+
width: 24px;
|
| 515 |
+
height: 24px;
|
| 516 |
+
border-radius: 50%;
|
| 517 |
+
cursor: pointer;
|
| 518 |
+
display: flex;
|
| 519 |
+
align-items: center;
|
| 520 |
+
justify-content: center;
|
| 521 |
+
font-size: 12px;
|
| 522 |
+
}
|
| 523 |
+
|
| 524 |
+
.close-tablet:hover {
|
| 525 |
+
background: var(--burnished-bronze);
|
| 526 |
+
color: var(--deep-earth);
|
| 527 |
+
}
|
| 528 |
+
|
| 529 |
+
.track-list {
|
| 530 |
+
display: flex;
|
| 531 |
+
flex-direction: column;
|
| 532 |
+
gap: 8px;
|
| 533 |
+
}
|
| 534 |
+
|
| 535 |
+
.track-item {
|
| 536 |
+
display: flex;
|
| 537 |
+
align-items: center;
|
| 538 |
+
gap: 10px;
|
| 539 |
+
padding: 6px 10px;
|
| 540 |
+
border-radius: 4px;
|
| 541 |
+
cursor: pointer;
|
| 542 |
+
transition: all 0.2s ease;
|
| 543 |
+
border: 1px solid transparent;
|
| 544 |
+
}
|
| 545 |
+
|
| 546 |
+
.track-item:hover {
|
| 547 |
+
background: var(--stone-gray);
|
| 548 |
+
border-color: var(--burnished-bronze);
|
| 549 |
+
}
|
| 550 |
+
|
| 551 |
+
.track-item.active {
|
| 552 |
+
background: var(--burnished-bronze);
|
| 553 |
+
border-color: var(--mystical-gold);
|
| 554 |
+
}
|
| 555 |
+
|
| 556 |
+
.track-item.active .track-name {
|
| 557 |
+
color: var(--mystical-gold);
|
| 558 |
+
}
|
| 559 |
+
|
| 560 |
+
.track-number {
|
| 561 |
+
font-size: 12px;
|
| 562 |
+
color: var(--bronze-warm);
|
| 563 |
+
min-width: 20px;
|
| 564 |
+
}
|
| 565 |
+
|
| 566 |
+
.track-details {
|
| 567 |
+
flex: 1;
|
| 568 |
+
}
|
| 569 |
+
|
| 570 |
+
.track-name {
|
| 571 |
+
font-size: 12px;
|
| 572 |
+
color: var(--copper-light);
|
| 573 |
+
margin-bottom: 1px;
|
| 574 |
+
}
|
| 575 |
+
|
| 576 |
+
/* Social Glyphs Component Styles */
|
| 577 |
+
.social-glyphs {
|
| 578 |
+
position: fixed;
|
| 579 |
+
top: 20px;
|
| 580 |
+
right: 20px;
|
| 581 |
+
display: flex;
|
| 582 |
+
flex-direction: column;
|
| 583 |
+
gap: 15px;
|
| 584 |
+
z-index: 1000;
|
| 585 |
+
}
|
| 586 |
+
|
| 587 |
+
.social-glyph {
|
| 588 |
+
width: 40px;
|
| 589 |
+
height: 40px;
|
| 590 |
+
border-radius: 8px;
|
| 591 |
+
background: var(--earth-dark);
|
| 592 |
+
border: 1px solid var(--bronze-warm);
|
| 593 |
+
display: flex;
|
| 594 |
+
align-items: center;
|
| 595 |
+
justify-content: center;
|
| 596 |
+
cursor: pointer;
|
| 597 |
+
transition: all 0.3s ease;
|
| 598 |
+
color: var(--bronze-warm);
|
| 599 |
+
font-size: 18px;
|
| 600 |
+
position: relative;
|
| 601 |
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
| 602 |
+
}
|
| 603 |
+
|
| 604 |
+
.social-glyph:hover {
|
| 605 |
+
background: var(--bronze-warm);
|
| 606 |
+
color: var(--stellar-copper);
|
| 607 |
+
transform: scale(1.1);
|
| 608 |
+
border-color: var(--stellar-copper);
|
| 609 |
+
}
|
| 610 |
+
|
| 611 |
+
.social-glyph::before {
|
| 612 |
+
content: '';
|
| 613 |
+
position: absolute;
|
| 614 |
+
top: -1px;
|
| 615 |
+
left: -1px;
|
| 616 |
+
right: -1px;
|
| 617 |
+
bottom: -1px;
|
| 618 |
+
background: linear-gradient(45deg, var(--stellar-copper), var(--bronze-warm));
|
| 619 |
+
border-radius: 9px;
|
| 620 |
+
z-index: -1;
|
| 621 |
+
opacity: 0;
|
| 622 |
+
transition: opacity 0.3s ease;
|
| 623 |
+
}
|
| 624 |
+
|
| 625 |
+
.social-glyph:hover::before {
|
| 626 |
+
opacity: 0.2;
|
| 627 |
+
}
|
| 628 |
+
|
| 629 |
+
.tooltip {
|
| 630 |
+
position: absolute;
|
| 631 |
+
bottom: 120%;
|
| 632 |
+
left: 50%;
|
| 633 |
+
transform: translateX(-50%);
|
| 634 |
+
background: var(--earth-dark);
|
| 635 |
+
color: var(--copper-light);
|
| 636 |
+
padding: 8px 12px;
|
| 637 |
+
border-radius: 6px;
|
| 638 |
+
border: 1px solid var(--bronze-warm);
|
| 639 |
+
font-size: 12px;
|
| 640 |
+
white-space: nowrap;
|
| 641 |
+
opacity: 1;
|
| 642 |
+
pointer-events: none;
|
| 643 |
+
z-index: 1001;
|
| 644 |
+
font-family: 'Cinzel', serif;
|
| 645 |
+
letter-spacing: 1px;
|
| 646 |
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
|
| 647 |
+
}
|
| 648 |
+
|
| 649 |
+
.tooltip::after {
|
| 650 |
+
content: '';
|
| 651 |
+
position: absolute;
|
| 652 |
+
top: 100%;
|
| 653 |
+
left: 50%;
|
| 654 |
+
transform: translateX(-50%);
|
| 655 |
+
border: 5px solid transparent;
|
| 656 |
+
border-top-color: var(--bronze-warm);
|
| 657 |
+
}
|
| 658 |
+
|
| 659 |
+
/* Glyph Animation */
|
| 660 |
+
.social-glyph {
|
| 661 |
+
animation: glyphAppear 0.6s ease-out backwards;
|
| 662 |
+
}
|
| 663 |
+
|
| 664 |
+
.social-glyph:nth-child(1) { animation-delay: 0.1s; }
|
| 665 |
+
.social-glyph:nth-child(2) { animation-delay: 0.2s; }
|
| 666 |
+
.social-glyph:nth-child(3) { animation-delay: 0.3s; }
|
| 667 |
+
.social-glyph:nth-child(4) { animation-delay: 0.4s; }
|
| 668 |
+
.social-glyph:nth-child(5) { animation-delay: 0.5s; }
|
| 669 |
+
|
| 670 |
+
@keyframes glyphAppear {
|
| 671 |
+
0% {
|
| 672 |
+
opacity: 0;
|
| 673 |
+
transform: scale(0.5) translateY(20px);
|
| 674 |
+
}
|
| 675 |
+
100% {
|
| 676 |
+
opacity: 1;
|
| 677 |
+
transform: scale(1) translateY(0);
|
| 678 |
+
}
|
| 679 |
+
}
|
| 680 |
+
|
| 681 |
+
/* Platform-specific subtle effects */
|
| 682 |
+
.social-glyph[data-platform="spotify"]:hover {
|
| 683 |
+
border-color: #1DB954;
|
| 684 |
+
}
|
| 685 |
+
|
| 686 |
+
.social-glyph[data-platform="apple"]:hover {
|
| 687 |
+
border-color: #FA57C1;
|
| 688 |
+
}
|
| 689 |
+
|
| 690 |
+
.social-glyph[data-platform="instagram"]:hover {
|
| 691 |
+
border-color: #E4405F;
|
| 692 |
+
}
|
| 693 |
+
|
| 694 |
+
.social-glyph[data-platform="youtube"]:hover {
|
| 695 |
+
border-color: #FF0000;
|
| 696 |
+
}
|
| 697 |
+
|
| 698 |
+
.social-glyph[data-platform="twitter"]:hover {
|
| 699 |
+
border-color: #1DA1F2;
|
| 700 |
+
}
|
| 701 |
+
|
| 702 |
+
@keyframes energyFlow {
|
| 703 |
+
0% { stroke-dashoffset: 0; }
|
| 704 |
+
100% { stroke-dashoffset: 40; }
|
| 705 |
+
}
|
| 706 |
+
|
| 707 |
+
/* Control Plinth Styles */
|
| 708 |
+
.control-plinth {
|
| 709 |
+
position: relative;
|
| 710 |
+
margin: 15px auto;
|
| 711 |
+
background: rgba(15, 27, 19, 0.9);
|
| 712 |
+
border: 1px solid rgba(166, 123, 91, 0.3);
|
| 713 |
+
border-radius: 8px;
|
| 714 |
+
padding: 15px 25px;
|
| 715 |
+
box-shadow:
|
| 716 |
+
0 4px 20px rgba(0, 0, 0, 0.8),
|
| 717 |
+
inset 0 1px 3px rgba(166, 123, 91, 0.1);
|
| 718 |
+
backdrop-filter: blur(15px);
|
| 719 |
+
max-width: 700px;
|
| 720 |
+
width: 85%;
|
| 721 |
+
}
|
| 722 |
+
|
| 723 |
+
.control-plinth::before {
|
| 724 |
+
content: '';
|
| 725 |
+
position: absolute;
|
| 726 |
+
top: -2px;
|
| 727 |
+
left: -2px;
|
| 728 |
+
right: -2px;
|
| 729 |
+
bottom: -2px;
|
| 730 |
+
background: var(--forest-gradient);
|
| 731 |
+
border-radius: 14px;
|
| 732 |
+
z-index: -1;
|
| 733 |
+
opacity: 0.3;
|
| 734 |
+
animation: plinthGlow 6s ease-in-out infinite;
|
| 735 |
+
}
|
| 736 |
+
|
| 737 |
+
@keyframes plinthGlow {
|
| 738 |
+
0%, 100% { opacity: 0.2; }
|
| 739 |
+
50% { opacity: 0.3; }
|
| 740 |
+
}
|
| 741 |
+
|
| 742 |
+
/* Plinth Controls */
|
| 743 |
+
.plinth-control {
|
| 744 |
+
background: transparent;
|
| 745 |
+
border: 1px solid var(--bronze-warm);
|
| 746 |
+
color: var(--copper-light);
|
| 747 |
+
padding: 8px 16px;
|
| 748 |
+
border-radius: 6px;
|
| 749 |
+
font-family: 'Cinzel', serif;
|
| 750 |
+
font-size: 14px;
|
| 751 |
+
cursor: pointer;
|
| 752 |
+
transition: all 0.3s ease;
|
| 753 |
+
margin: 0 5px;
|
| 754 |
+
}
|
| 755 |
+
|
| 756 |
+
.plinth-control:hover {
|
| 757 |
+
background: var(--bronze-warm);
|
| 758 |
+
color: var(--forest-deep);
|
| 759 |
+
transform: translateY(-2px);
|
| 760 |
+
}
|
| 761 |
+
|
| 762 |
+
.plinth-control:active {
|
| 763 |
+
transform: translateY(0px);
|
| 764 |
+
}
|
| 765 |
+
|
| 766 |
+
/* Play/Pause Central Glyph */
|
| 767 |
+
.central-glyph {
|
| 768 |
+
width: 60px;
|
| 769 |
+
height: 60px;
|
| 770 |
+
border-radius: 50%;
|
| 771 |
+
background: var(--copper-gradient);
|
| 772 |
+
border: 3px solid var(--stellar-copper);
|
| 773 |
+
cursor: pointer;
|
| 774 |
+
display: flex;
|
| 775 |
+
align-items: center;
|
| 776 |
+
justify-content: center;
|
| 777 |
+
font-size: 24px;
|
| 778 |
+
color: var(--forest-deep);
|
| 779 |
+
transition: all 0.3s ease;
|
| 780 |
+
margin: 0 15px;
|
| 781 |
+
}
|
| 782 |
+
|
| 783 |
+
.central-glyph:hover {
|
| 784 |
+
transform: scale(1.1);
|
| 785 |
+
background: var(--nebula-gradient);
|
| 786 |
+
}
|
| 787 |
+
|
| 788 |
+
/* Timeline Scrubber */
|
| 789 |
+
.timeline-channel {
|
| 790 |
+
width: 300px;
|
| 791 |
+
height: 8px;
|
| 792 |
+
background: var(--bark-brown);
|
| 793 |
+
border-radius: 4px;
|
| 794 |
+
position: relative;
|
| 795 |
+
margin: 0 15px;
|
| 796 |
+
border: 1px solid var(--bronze-warm);
|
| 797 |
+
overflow: hidden;
|
| 798 |
+
}
|
| 799 |
+
|
| 800 |
+
.timeline-indicator {
|
| 801 |
+
height: 100%;
|
| 802 |
+
background: var(--copper-gradient);
|
| 803 |
+
border-radius: 4px;
|
| 804 |
+
transition: width 0.1s ease;
|
| 805 |
+
}
|
| 806 |
+
|
| 807 |
+
.timeline-scrubber {
|
| 808 |
+
-webkit-appearance: none;
|
| 809 |
+
width: 100%;
|
| 810 |
+
height: 8px;
|
| 811 |
+
background: transparent;
|
| 812 |
+
position: absolute;
|
| 813 |
+
top: 0;
|
| 814 |
+
left: 0;
|
| 815 |
+
cursor: pointer;
|
| 816 |
+
}
|
| 817 |
+
|
| 818 |
+
.timeline-scrubber::-webkit-slider-thumb {
|
| 819 |
+
-webkit-appearance: none;
|
| 820 |
+
width: 16px;
|
| 821 |
+
height: 16px;
|
| 822 |
+
border-radius: 50%;
|
| 823 |
+
background: var(--stellar-copper);
|
| 824 |
+
cursor: pointer;
|
| 825 |
+
}
|
| 826 |
+
|
| 827 |
+
/* Volume Control */
|
| 828 |
+
.volume-dial {
|
| 829 |
+
width: 40px;
|
| 830 |
+
height: 40px;
|
| 831 |
+
border-radius: 50%;
|
| 832 |
+
border: 2px solid var(--bronze-warm);
|
| 833 |
+
background: var(--earth-dark);
|
| 834 |
+
position: relative;
|
| 835 |
+
cursor: pointer;
|
| 836 |
+
}
|
| 837 |
+
|
| 838 |
+
.volume-indicator {
|
| 839 |
+
position: absolute;
|
| 840 |
+
top: 50%;
|
| 841 |
+
left: 50%;
|
| 842 |
+
width: 2px;
|
| 843 |
+
height: 16px;
|
| 844 |
+
background: var(--stellar-copper);
|
| 845 |
+
transform-origin: bottom center;
|
| 846 |
+
transform: translate(-50%, -100%);
|
| 847 |
+
transition: transform 0.3s ease;
|
| 848 |
+
}
|
| 849 |
+
|
| 850 |
+
/* Social Glyphs */
|
| 851 |
+
.social-glyphs {
|
| 852 |
+
position: fixed;
|
| 853 |
+
bottom: 20px;
|
| 854 |
+
right: 20px;
|
| 855 |
+
display: flex;
|
| 856 |
+
flex-direction: column;
|
| 857 |
+
gap: 15px;
|
| 858 |
+
z-index: 1000;
|
| 859 |
+
}
|
| 860 |
+
|
| 861 |
+
.social-glyph {
|
| 862 |
+
width: 40px;
|
| 863 |
+
height: 40px;
|
| 864 |
+
border-radius: 8px;
|
| 865 |
+
background: var(--deep-earth);
|
| 866 |
+
border: 1px solid var(--burnished-bronze);
|
| 867 |
+
display: flex;
|
| 868 |
+
align-items: center;
|
| 869 |
+
justify-content: center;
|
| 870 |
+
cursor: pointer;
|
| 871 |
+
transition: all 0.3s ease;
|
| 872 |
+
color: var(--burnished-bronze);
|
| 873 |
+
}
|
| 874 |
+
|
| 875 |
+
.social-glyph:hover {
|
| 876 |
+
background: var(--burnished-bronze);
|
| 877 |
+
color: var(--mystical-gold);
|
| 878 |
+
box-shadow: 0 0 15px var(--mystical-gold);
|
| 879 |
+
transform: scale(1.1);
|
| 880 |
+
}
|
| 881 |
+
|
| 882 |
+
/* Tooltip */
|
| 883 |
+
.tooltip {
|
| 884 |
+
position: absolute;
|
| 885 |
+
bottom: 120%;
|
| 886 |
+
left: 50%;
|
| 887 |
+
transform: translateX(-50%);
|
| 888 |
+
background: var(--void-black);
|
| 889 |
+
color: var(--ancient-white);
|
| 890 |
+
padding: 8px 12px;
|
| 891 |
+
border-radius: 6px;
|
| 892 |
+
border: 1px solid var(--burnished-bronze);
|
| 893 |
+
font-size: 12px;
|
| 894 |
+
white-space: nowrap;
|
| 895 |
+
opacity: 0;
|
| 896 |
+
pointer-events: none;
|
| 897 |
+
transition: opacity 0.3s ease;
|
| 898 |
+
z-index: 1001;
|
| 899 |
+
}
|
| 900 |
+
|
| 901 |
+
.social-glyph:hover .tooltip {
|
| 902 |
+
opacity: 1;
|
| 903 |
+
}
|
| 904 |
+
|
| 905 |
+
/* Responsive Design */
|
| 906 |
+
@media (max-width: 768px) {
|
| 907 |
+
.control-plinth {
|
| 908 |
+
margin: 10px auto;
|
| 909 |
+
padding: 15px 20px;
|
| 910 |
+
width: 95%;
|
| 911 |
+
}
|
| 912 |
+
|
| 913 |
+
.timeline-channel {
|
| 914 |
+
width: 200px;
|
| 915 |
+
}
|
| 916 |
+
|
| 917 |
+
.central-glyph {
|
| 918 |
+
width: 50px;
|
| 919 |
+
height: 50px;
|
| 920 |
+
font-size: 20px;
|
| 921 |
+
}
|
| 922 |
+
|
| 923 |
+
.social-glyphs {
|
| 924 |
+
bottom: 10px;
|
| 925 |
+
right: 10px;
|
| 926 |
+
}
|
| 927 |
+
}
|
| 928 |
+
|
| 929 |
+
/* Loading Animation */
|
| 930 |
+
.awakening {
|
| 931 |
+
animation: awakeningSequence 2s ease-out forwards;
|
| 932 |
+
}
|
| 933 |
+
|
| 934 |
+
@keyframes awakeningSequence {
|
| 935 |
+
0% {
|
| 936 |
+
opacity: 0.3;
|
| 937 |
+
filter: brightness(0.5);
|
| 938 |
+
}
|
| 939 |
+
50% {
|
| 940 |
+
opacity: 0.7;
|
| 941 |
+
filter: brightness(1.2);
|
| 942 |
+
}
|
| 943 |
+
100% {
|
| 944 |
+
opacity: 1;
|
| 945 |
+
filter: brightness(1);
|
| 946 |
+
}
|
| 947 |
+
}
|
| 948 |
+
|
| 949 |
+
/* Dormant State */
|
| 950 |
+
.dormant {
|
| 951 |
+
opacity: 0.4;
|
| 952 |
+
filter: grayscale(0.5);
|
| 953 |
+
animation: dormantBreath 8s ease-in-out infinite;
|
| 954 |
+
}
|
| 955 |
+
|
| 956 |
+
@keyframes dormantBreath {
|
| 957 |
+
0%, 100% {
|
| 958 |
+
opacity: 0.3;
|
| 959 |
+
transform: scale(0.98);
|
| 960 |
+
}
|
| 961 |
+
50% {
|
| 962 |
+
opacity: 0.5;
|
| 963 |
+
transform: scale(1);
|
| 964 |
+
}
|
| 965 |
+
}
|
src/types/audio.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export interface FrequencyMapping {
|
| 2 |
+
deepFreq: { min: number; max: number }; // 20-250Hz - Deep Earth Pulse
|
| 3 |
+
midFreq: { min: number; max: number }; // 250-4000Hz - Heartwood Resonance
|
| 4 |
+
highFreq: { min: number; max: number }; // 4000-20000Hz - Canopy Shiver
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
export interface AudioData {
|
| 8 |
+
frequencies: Uint8Array;
|
| 9 |
+
deepEnergy: number; // Normalized 0-1
|
| 10 |
+
midEnergy: number; // Normalized 0-1
|
| 11 |
+
highEnergy: number; // Normalized 0-1
|
| 12 |
+
overallAmplitude: number; // Normalized 0-1
|
| 13 |
+
peakDetected: boolean;
|
| 14 |
+
beatDetected: boolean;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
export interface PeakDetection {
|
| 18 |
+
energyHistory: number[];
|
| 19 |
+
historyLength: number;
|
| 20 |
+
lastPeakTime: number;
|
| 21 |
+
minTimeBetweenPeaks: number;
|
| 22 |
+
sensitivity: number;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
export interface BeatManager {
|
| 26 |
+
currentWaveRadius: number;
|
| 27 |
+
waveStrength: number;
|
| 28 |
+
isWaveActive: boolean;
|
| 29 |
+
triggerWave: (energy: number) => void;
|
| 30 |
+
update: (deltaTime: number) => void;
|
| 31 |
+
getWaveForce: (distance: number) => number;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
export interface Track {
|
| 35 |
+
id: string;
|
| 36 |
+
title: string;
|
| 37 |
+
artist: string;
|
| 38 |
+
url: string;
|
| 39 |
+
duration?: number;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
export interface PlaybackState {
|
| 43 |
+
isPlaying: boolean;
|
| 44 |
+
currentTrack: Track | null;
|
| 45 |
+
currentTime: number;
|
| 46 |
+
volume: number;
|
| 47 |
+
tracks: Track[];
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
export interface AudioEngine {
|
| 51 |
+
initialize: () => Promise<void>;
|
| 52 |
+
loadTrack: (track: Track) => Promise<void>;
|
| 53 |
+
play: () => Promise<void>;
|
| 54 |
+
pause: () => void;
|
| 55 |
+
seekTo: (time: number) => void;
|
| 56 |
+
setVolume: (volume: number) => void;
|
| 57 |
+
getAudioData: () => AudioData;
|
| 58 |
+
getCurrentTime: () => number;
|
| 59 |
+
getDuration: () => number;
|
| 60 |
+
getAudioElement: () => HTMLAudioElement | null;
|
| 61 |
+
isPlaying: () => boolean;
|
| 62 |
+
cleanup: () => void;
|
| 63 |
+
setOnTrackEndedCallback: (callback: (() => void) | null) => void;
|
| 64 |
+
}
|
src/types/visualization.ts
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Enhanced visualization types for advanced particle system
|
| 2 |
+
|
| 3 |
+
export interface ParticleSystemParams {
|
| 4 |
+
enabled: boolean
|
| 5 |
+
sphereRadius: number
|
| 6 |
+
innerSphereRadius: number
|
| 7 |
+
rotationSpeed: number
|
| 8 |
+
rotationSpeedMin: number
|
| 9 |
+
rotationSpeedMax: number
|
| 10 |
+
rotationSmoothness: number
|
| 11 |
+
particleCount: number
|
| 12 |
+
particleSize: number
|
| 13 |
+
particleLifetime: number
|
| 14 |
+
minFrequency: number
|
| 15 |
+
maxFrequency: number
|
| 16 |
+
minFrequencyBeat: number
|
| 17 |
+
maxFrequencyBeat: number
|
| 18 |
+
noiseScale: number
|
| 19 |
+
dynamicNoiseScale: boolean
|
| 20 |
+
minNoiseScale: number
|
| 21 |
+
maxNoiseScale: number
|
| 22 |
+
noiseStep: number
|
| 23 |
+
noiseSpeed: number
|
| 24 |
+
turbulenceStrength: number
|
| 25 |
+
colorStart: string
|
| 26 |
+
colorEnd: string
|
| 27 |
+
volumeChangeThreshold: number
|
| 28 |
+
peakSensitivity: number
|
| 29 |
+
beatThreshold: number
|
| 30 |
+
baseWaveStrength: number
|
| 31 |
+
beatStrength: number
|
| 32 |
+
gainMultiplier: number
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
export interface PeakDetection {
|
| 36 |
+
energyHistory: number[]
|
| 37 |
+
historyLength: number
|
| 38 |
+
lastPeakTime: number
|
| 39 |
+
minTimeBetweenPeaks: number
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
export interface ParticleSphere {
|
| 43 |
+
index: number
|
| 44 |
+
params: ParticleSystemParams
|
| 45 |
+
positions: Float32Array
|
| 46 |
+
velocities: Float32Array
|
| 47 |
+
basePositions: Float32Array
|
| 48 |
+
lifetimes: Float32Array
|
| 49 |
+
maxLifetimes: Float32Array
|
| 50 |
+
beatEffects: Float32Array
|
| 51 |
+
colors: Float32Array
|
| 52 |
+
lastNoiseScale: number
|
| 53 |
+
lastValidVolume: number
|
| 54 |
+
lastRotationSpeed: number
|
| 55 |
+
peakDetection: PeakDetection
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
export interface AudioAnalysisData {
|
| 59 |
+
average: number
|
| 60 |
+
frequencies: Uint8Array
|
| 61 |
+
peakDetected: boolean
|
| 62 |
+
rangeEnergy: number
|
| 63 |
+
rangeEnergyBeat: number
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
export interface FogParams {
|
| 67 |
+
enabled: boolean
|
| 68 |
+
color: string
|
| 69 |
+
near: number
|
| 70 |
+
far: number
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
export interface VisualizationPreset {
|
| 74 |
+
name: string
|
| 75 |
+
spheres: ParticleSystemParams[]
|
| 76 |
+
fog?: FogParams
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
export interface VisualizationState {
|
| 80 |
+
// Enhanced particle system state
|
| 81 |
+
spheres: ParticleSphere[]
|
| 82 |
+
beatManager?: {
|
| 83 |
+
currentWaveRadius: number
|
| 84 |
+
waveStrength: number
|
| 85 |
+
isWaveActive: boolean
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
// Legacy tree visualization state (for backward compatibility)
|
| 89 |
+
deepPulseIntensity: number
|
| 90 |
+
midGlowIntensity: number
|
| 91 |
+
highShimmerIntensity: number
|
| 92 |
+
worldBreathRate: number
|
| 93 |
+
|
| 94 |
+
// Animation state
|
| 95 |
+
lastTime: number
|
| 96 |
+
isAnimating: boolean
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
// Default frequency ranges for different spheres
|
| 100 |
+
export const DEFAULT_FREQUENCY_RANGES = [
|
| 101 |
+
{ minFrequency: 20, maxFrequency: 80 }, // Sub-bass
|
| 102 |
+
{ minFrequency: 120, maxFrequency: 250 }, // Bass
|
| 103 |
+
{ minFrequency: 250, maxFrequency: 800 }, // Mid
|
| 104 |
+
{ minFrequency: 1000, maxFrequency: 4000 }, // High mid
|
| 105 |
+
{ minFrequency: 5000, maxFrequency: 10000 } // High
|
| 106 |
+
] as const
|
| 107 |
+
|
| 108 |
+
export const DEFAULT_SPHERE_PARAMS: ParticleSystemParams = {
|
| 109 |
+
enabled: false,
|
| 110 |
+
sphereRadius: 1.0,
|
| 111 |
+
innerSphereRadius: 0.25,
|
| 112 |
+
rotationSpeed: 0.001,
|
| 113 |
+
rotationSpeedMin: 0,
|
| 114 |
+
rotationSpeedMax: 0.065,
|
| 115 |
+
rotationSmoothness: 0.3,
|
| 116 |
+
particleCount: 20000,
|
| 117 |
+
particleSize: 0.003,
|
| 118 |
+
particleLifetime: 3.0,
|
| 119 |
+
minFrequency: 20,
|
| 120 |
+
maxFrequency: 80,
|
| 121 |
+
minFrequencyBeat: 20,
|
| 122 |
+
maxFrequencyBeat: 80,
|
| 123 |
+
noiseScale: 4.0,
|
| 124 |
+
dynamicNoiseScale: true,
|
| 125 |
+
minNoiseScale: 0.5,
|
| 126 |
+
maxNoiseScale: 5.0,
|
| 127 |
+
noiseStep: 0.2,
|
| 128 |
+
noiseSpeed: 0.1,
|
| 129 |
+
turbulenceStrength: 0.005,
|
| 130 |
+
colorStart: '#ff3366',
|
| 131 |
+
colorEnd: '#3366ff',
|
| 132 |
+
volumeChangeThreshold: 0.1,
|
| 133 |
+
peakSensitivity: 1.1,
|
| 134 |
+
beatThreshold: 200,
|
| 135 |
+
baseWaveStrength: 20.0,
|
| 136 |
+
beatStrength: 0.01,
|
| 137 |
+
gainMultiplier: 1
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
// Legacy types for backward compatibility
|
| 141 |
+
export interface WorldTreeNode {
|
| 142 |
+
id: string;
|
| 143 |
+
x: number;
|
| 144 |
+
y: number;
|
| 145 |
+
size: number;
|
| 146 |
+
glyphType: 'root' | 'trunk' | 'branch' | 'leaf';
|
| 147 |
+
intensity: number; // 0-1, driven by audio
|
| 148 |
+
connections: string[]; // IDs of connected nodes
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
export interface TreeBranch {
|
| 152 |
+
id: string;
|
| 153 |
+
startNode: string;
|
| 154 |
+
endNode: string;
|
| 155 |
+
strokeWidth: number;
|
| 156 |
+
pathData: string; // SVG path
|
| 157 |
+
energy: number; // Current energy flowing through
|
| 158 |
+
pulsePhase: number; // Animation phase
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
export interface GlyphSystem {
|
| 162 |
+
coreGlyphs: WorldTreeNode[];
|
| 163 |
+
arterialPathways: TreeBranch[];
|
| 164 |
+
fractalBranches: TreeBranch[];
|
| 165 |
+
emberParticles: Particle[];
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
export interface Particle {
|
| 169 |
+
id: string;
|
| 170 |
+
x: number;
|
| 171 |
+
y: number;
|
| 172 |
+
vx: number;
|
| 173 |
+
vy: number;
|
| 174 |
+
life: number;
|
| 175 |
+
maxLife: number;
|
| 176 |
+
size: number;
|
| 177 |
+
opacity: number;
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
export interface VisualizationParams {
|
| 181 |
+
worldBreathRate: number; // Base breathing animation speed
|
| 182 |
+
deepPulseIntensity: number; // Trunk/root pulse strength
|
| 183 |
+
midGlowIntensity: number; // Core glyph glow
|
| 184 |
+
highShimmerCount: number; // Number of fractal branches/particles
|
| 185 |
+
noiseScale: number; // Perlin noise scale for organic movement
|
| 186 |
+
colorPalette: {
|
| 187 |
+
primary: string; // Deep earth tones
|
| 188 |
+
secondary: string; // Burnished metal
|
| 189 |
+
accent: string; // Mystical glow
|
| 190 |
+
ember: string; // Particle color
|
| 191 |
+
};
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
export interface InteractionState {
|
| 195 |
+
isDormant: boolean;
|
| 196 |
+
isAwakening: boolean;
|
| 197 |
+
isAlive: boolean;
|
| 198 |
+
lastInteraction: number;
|
| 199 |
+
}
|
src/utils/configLoader.ts
ADDED
|
@@ -0,0 +1,632 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Configuration loader and manager for the visualizer
|
| 2 |
+
import type { ParticleSystemParams } from '../types/visualization'
|
| 3 |
+
|
| 4 |
+
export interface VisualizerConfig {
|
| 5 |
+
visualization: {
|
| 6 |
+
general: {
|
| 7 |
+
maxSpheres: number
|
| 8 |
+
defaultActiveSpheres: number
|
| 9 |
+
performanceMode: boolean
|
| 10 |
+
debugLogging: boolean
|
| 11 |
+
debugLogFrequency: number
|
| 12 |
+
}
|
| 13 |
+
particles: {
|
| 14 |
+
defaultParticleCount: number
|
| 15 |
+
minParticleCount: number
|
| 16 |
+
maxParticleCount: number
|
| 17 |
+
particleSize: number
|
| 18 |
+
particleLifetime: number
|
| 19 |
+
particleSizeVariation: number
|
| 20 |
+
particleOpacity: number
|
| 21 |
+
}
|
| 22 |
+
spheres: {
|
| 23 |
+
baseRadius: number
|
| 24 |
+
radiusIncrement: number
|
| 25 |
+
innerSphereRadius: number
|
| 26 |
+
maxSphereRadius: number
|
| 27 |
+
containmentPullback: number
|
| 28 |
+
overflowDamping: number
|
| 29 |
+
}
|
| 30 |
+
physics: {
|
| 31 |
+
turbulenceStrength: number
|
| 32 |
+
noiseScale: number
|
| 33 |
+
noiseScaleVariation: number
|
| 34 |
+
noiseSpeed: number
|
| 35 |
+
defaultDamping: number
|
| 36 |
+
velocityDecay: number
|
| 37 |
+
beatForceMultiplier: number
|
| 38 |
+
maxVelocity: number
|
| 39 |
+
stabilityThreshold: number
|
| 40 |
+
emergencyDamping: number
|
| 41 |
+
}
|
| 42 |
+
rotation: {
|
| 43 |
+
rotationSpeedMin: number
|
| 44 |
+
rotationSpeedMax: number
|
| 45 |
+
rotationSpeedIncrement: number
|
| 46 |
+
rotationSmoothness: number
|
| 47 |
+
autoRotateSpeed: number
|
| 48 |
+
}
|
| 49 |
+
}
|
| 50 |
+
audio: {
|
| 51 |
+
frequencyRanges: Array<{
|
| 52 |
+
name: string
|
| 53 |
+
minFrequency: number
|
| 54 |
+
maxFrequency: number
|
| 55 |
+
beatThreshold: number
|
| 56 |
+
volumeChangeThreshold: number
|
| 57 |
+
}>
|
| 58 |
+
beatDetection: {
|
| 59 |
+
energyHistoryLength: number
|
| 60 |
+
minTimeBetweenPeaks: number
|
| 61 |
+
beatStrength: number
|
| 62 |
+
waveDecayRate: number
|
| 63 |
+
peakThresholdMultiplier: number
|
| 64 |
+
}
|
| 65 |
+
volumeSmoothing: {
|
| 66 |
+
enabled: boolean
|
| 67 |
+
smoothingFactor: number
|
| 68 |
+
changeThresholdDefault: number
|
| 69 |
+
}
|
| 70 |
+
}
|
| 71 |
+
entronaut: {
|
| 72 |
+
enabled: boolean
|
| 73 |
+
sefaAnalysis: {
|
| 74 |
+
fieldSize: number
|
| 75 |
+
fieldWidth: number
|
| 76 |
+
fieldHeight: number
|
| 77 |
+
updateFrequency: number
|
| 78 |
+
temporalSmoothing: number
|
| 79 |
+
intensitySmoothing: number
|
| 80 |
+
}
|
| 81 |
+
weights: {
|
| 82 |
+
alpha_amplitude: number
|
| 83 |
+
alpha_phase: number
|
| 84 |
+
alpha_entropy: number
|
| 85 |
+
alpha_curvature: number
|
| 86 |
+
alpha_spectral: number
|
| 87 |
+
}
|
| 88 |
+
adaptiveCoupling: {
|
| 89 |
+
enabled: boolean
|
| 90 |
+
updateFrequency: number
|
| 91 |
+
dampingBase: number
|
| 92 |
+
dampingVariation: number
|
| 93 |
+
diffusionBase: number
|
| 94 |
+
diffusionVariation: number
|
| 95 |
+
couplingBase: number
|
| 96 |
+
couplingVariation: number
|
| 97 |
+
flockingRadius: number
|
| 98 |
+
flockingInfluence: number
|
| 99 |
+
metabolicPulseStrength: number
|
| 100 |
+
neighborCheckStep: number
|
| 101 |
+
neighborRange: number
|
| 102 |
+
}
|
| 103 |
+
biomimeticBehavior: {
|
| 104 |
+
collectiveMemory: number
|
| 105 |
+
emergenceThreshold: number
|
| 106 |
+
coherenceWeight: number
|
| 107 |
+
complexityWeight: number
|
| 108 |
+
networkInfluence: number
|
| 109 |
+
syncPulseStrength: number
|
| 110 |
+
}
|
| 111 |
+
}
|
| 112 |
+
tendrils: {
|
| 113 |
+
enabled: boolean
|
| 114 |
+
maxTendrils: number
|
| 115 |
+
defaultDensity: number
|
| 116 |
+
maxTendrilLength: number
|
| 117 |
+
tendrilSegments: number
|
| 118 |
+
updateInterval: number
|
| 119 |
+
sampleParticlesPerSphere: number
|
| 120 |
+
particleSampleStep: number
|
| 121 |
+
curvature: {
|
| 122 |
+
midPointMultiplier: number
|
| 123 |
+
undulationStrength: number
|
| 124 |
+
timeFactorBase: number
|
| 125 |
+
timeFactorCurve: number
|
| 126 |
+
spatialFrequency: number
|
| 127 |
+
}
|
| 128 |
+
visual: {
|
| 129 |
+
baseOpacity: number
|
| 130 |
+
glowIntensity: number
|
| 131 |
+
colorIntensityBase: number
|
| 132 |
+
colorIntensityVariation: number
|
| 133 |
+
blendingMode: string
|
| 134 |
+
}
|
| 135 |
+
}
|
| 136 |
+
cymatics: {
|
| 137 |
+
enabled: boolean
|
| 138 |
+
intensity: number
|
| 139 |
+
patterns: {
|
| 140 |
+
radialWaves: {
|
| 141 |
+
frequencyFactor: number
|
| 142 |
+
petalCount: number
|
| 143 |
+
amplitudeZ: number
|
| 144 |
+
}
|
| 145 |
+
sphericalHarmonics: {
|
| 146 |
+
lMultiplier: number
|
| 147 |
+
mMultiplier: number
|
| 148 |
+
timeMultiplier: number
|
| 149 |
+
}
|
| 150 |
+
geometricLattice: {
|
| 151 |
+
frequencyFactor: number
|
| 152 |
+
}
|
| 153 |
+
flowerOfLife: {
|
| 154 |
+
frequencyFactor: number
|
| 155 |
+
basePetals: number
|
| 156 |
+
petalVariation: number
|
| 157 |
+
amplitudeZ: number
|
| 158 |
+
}
|
| 159 |
+
}
|
| 160 |
+
breathing: {
|
| 161 |
+
enabled: boolean
|
| 162 |
+
speed: number
|
| 163 |
+
intensityBase: number
|
| 164 |
+
intensityVariation: number
|
| 165 |
+
}
|
| 166 |
+
}
|
| 167 |
+
colors: {
|
| 168 |
+
sphereColorSchemes: Array<{
|
| 169 |
+
name: string
|
| 170 |
+
start: string
|
| 171 |
+
end: string
|
| 172 |
+
}>
|
| 173 |
+
tendrilColors: {
|
| 174 |
+
baseColor: [number, number, number]
|
| 175 |
+
frequencyColorMap: {
|
| 176 |
+
lowFreq: [number, number, number]
|
| 177 |
+
midFreq: [number, number, number]
|
| 178 |
+
highFreq: [number, number, number]
|
| 179 |
+
}
|
| 180 |
+
}
|
| 181 |
+
}
|
| 182 |
+
camera: {
|
| 183 |
+
defaultPosition: [number, number, number]
|
| 184 |
+
fov: number
|
| 185 |
+
near: number
|
| 186 |
+
far: number
|
| 187 |
+
controls: {
|
| 188 |
+
enableDamping: boolean
|
| 189 |
+
dampingFactor: number
|
| 190 |
+
maxDistance: number
|
| 191 |
+
minDistance: number
|
| 192 |
+
maxPolarAngle: number
|
| 193 |
+
autoRotateSpeed: number
|
| 194 |
+
}
|
| 195 |
+
presets: {
|
| 196 |
+
[key: string]: [number, number, number]
|
| 197 |
+
}
|
| 198 |
+
}
|
| 199 |
+
fog: {
|
| 200 |
+
enabled: boolean
|
| 201 |
+
color: string
|
| 202 |
+
near: number
|
| 203 |
+
far: number
|
| 204 |
+
density: number
|
| 205 |
+
}
|
| 206 |
+
rendering: {
|
| 207 |
+
antialias: boolean
|
| 208 |
+
background: string
|
| 209 |
+
particleBlending: string
|
| 210 |
+
targetFPS: number
|
| 211 |
+
adaptiveQuality: {
|
| 212 |
+
enabled: boolean
|
| 213 |
+
minFPS: number
|
| 214 |
+
qualityReductionStep: number
|
| 215 |
+
qualityRecoveryDelay: number
|
| 216 |
+
}
|
| 217 |
+
}
|
| 218 |
+
ui: {
|
| 219 |
+
controls: {
|
| 220 |
+
defaultCollapsed: boolean
|
| 221 |
+
position: string
|
| 222 |
+
maxWidth: string
|
| 223 |
+
opacity: number
|
| 224 |
+
}
|
| 225 |
+
about: {
|
| 226 |
+
defaultCollapsed: boolean
|
| 227 |
+
position: string
|
| 228 |
+
width: string
|
| 229 |
+
}
|
| 230 |
+
animation: {
|
| 231 |
+
transitionDuration: string
|
| 232 |
+
hoverScale: number
|
| 233 |
+
backdropBlur: string
|
| 234 |
+
}
|
| 235 |
+
}
|
| 236 |
+
performance: {
|
| 237 |
+
adaptiveSettings: {
|
| 238 |
+
enabled: boolean
|
| 239 |
+
thresholds: {
|
| 240 |
+
particleReduction: number
|
| 241 |
+
tendrilReduction: number
|
| 242 |
+
sefaDisable: number
|
| 243 |
+
couplingDisable: number
|
| 244 |
+
}
|
| 245 |
+
}
|
| 246 |
+
optimizations: {
|
| 247 |
+
particleUpdateStep: number
|
| 248 |
+
tendrilUpdateStep: number
|
| 249 |
+
sefaUpdateStep: number
|
| 250 |
+
couplingUpdateStep: number
|
| 251 |
+
neighborCheckReduction: number
|
| 252 |
+
}
|
| 253 |
+
}
|
| 254 |
+
presets: {
|
| 255 |
+
[key: string]: Partial<VisualizerConfig>
|
| 256 |
+
}
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
class ConfigLoader {
|
| 260 |
+
private config: VisualizerConfig | null = null
|
| 261 |
+
private loadPromise: Promise<VisualizerConfig> | null = null
|
| 262 |
+
|
| 263 |
+
async loadConfig(): Promise<VisualizerConfig> {
|
| 264 |
+
if (this.config) {
|
| 265 |
+
return this.config
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
if (this.loadPromise) {
|
| 269 |
+
return this.loadPromise
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
this.loadPromise = this.fetchConfig()
|
| 273 |
+
this.config = await this.loadPromise
|
| 274 |
+
return this.config
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
private async fetchConfig(): Promise<VisualizerConfig> {
|
| 278 |
+
try {
|
| 279 |
+
const response = await fetch('/config.json')
|
| 280 |
+
if (!response.ok) {
|
| 281 |
+
throw new Error(`Failed to load config: ${response.statusText}`)
|
| 282 |
+
}
|
| 283 |
+
const config = await response.json()
|
| 284 |
+
console.log('✅ Configuration loaded successfully')
|
| 285 |
+
return config
|
| 286 |
+
} catch (error) {
|
| 287 |
+
console.error('❌ Failed to load configuration, using defaults:', error)
|
| 288 |
+
return this.getDefaultConfig()
|
| 289 |
+
}
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
private getDefaultConfig(): VisualizerConfig {
|
| 293 |
+
// Fallback default configuration
|
| 294 |
+
return {
|
| 295 |
+
visualization: {
|
| 296 |
+
general: {
|
| 297 |
+
maxSpheres: 5,
|
| 298 |
+
defaultActiveSpheres: 5,
|
| 299 |
+
performanceMode: false,
|
| 300 |
+
debugLogging: true,
|
| 301 |
+
debugLogFrequency: 0.005
|
| 302 |
+
},
|
| 303 |
+
particles: {
|
| 304 |
+
defaultParticleCount: 15000,
|
| 305 |
+
minParticleCount: 1000,
|
| 306 |
+
maxParticleCount: 30000,
|
| 307 |
+
particleSize: 1.0,
|
| 308 |
+
particleLifetime: 20.0,
|
| 309 |
+
particleSizeVariation: 0.2,
|
| 310 |
+
particleOpacity: 0.8
|
| 311 |
+
},
|
| 312 |
+
spheres: {
|
| 313 |
+
baseRadius: 1.0,
|
| 314 |
+
radiusIncrement: 0.08,
|
| 315 |
+
innerSphereRadius: 0.7,
|
| 316 |
+
maxSphereRadius: 2.0,
|
| 317 |
+
containmentPullback: 0.1,
|
| 318 |
+
overflowDamping: 0.9
|
| 319 |
+
},
|
| 320 |
+
physics: {
|
| 321 |
+
turbulenceStrength: 0.008,
|
| 322 |
+
noiseScale: 3.0,
|
| 323 |
+
noiseScaleVariation: 0.4,
|
| 324 |
+
noiseSpeed: 0.5,
|
| 325 |
+
defaultDamping: 0.95,
|
| 326 |
+
velocityDecay: 0.95,
|
| 327 |
+
beatForceMultiplier: 0.001,
|
| 328 |
+
maxVelocity: 0.1,
|
| 329 |
+
stabilityThreshold: 10.0,
|
| 330 |
+
emergencyDamping: 0.1
|
| 331 |
+
},
|
| 332 |
+
rotation: {
|
| 333 |
+
rotationSpeedMin: 0.001,
|
| 334 |
+
rotationSpeedMax: 0.06,
|
| 335 |
+
rotationSpeedIncrement: 0.008,
|
| 336 |
+
rotationSmoothness: 0.1,
|
| 337 |
+
autoRotateSpeed: 0.5
|
| 338 |
+
}
|
| 339 |
+
},
|
| 340 |
+
audio: {
|
| 341 |
+
frequencyRanges: [
|
| 342 |
+
{ name: "sub-bass", minFrequency: 20, maxFrequency: 80, beatThreshold: 0.6, volumeChangeThreshold: 0.05 },
|
| 343 |
+
{ name: "bass", minFrequency: 120, maxFrequency: 250, beatThreshold: 0.65, volumeChangeThreshold: 0.08 },
|
| 344 |
+
{ name: "mid", minFrequency: 250, maxFrequency: 800, beatThreshold: 0.7, volumeChangeThreshold: 0.1 },
|
| 345 |
+
{ name: "high-mid", minFrequency: 1000, maxFrequency: 4000, beatThreshold: 0.75, volumeChangeThreshold: 0.12 },
|
| 346 |
+
{ name: "high", minFrequency: 5000, maxFrequency: 10000, beatThreshold: 0.8, volumeChangeThreshold: 0.15 }
|
| 347 |
+
],
|
| 348 |
+
beatDetection: {
|
| 349 |
+
energyHistoryLength: 30,
|
| 350 |
+
minTimeBetweenPeaks: 200,
|
| 351 |
+
beatStrength: 0.02,
|
| 352 |
+
waveDecayRate: 0.98,
|
| 353 |
+
peakThresholdMultiplier: 1.3
|
| 354 |
+
},
|
| 355 |
+
volumeSmoothing: {
|
| 356 |
+
enabled: true,
|
| 357 |
+
smoothingFactor: 0.1,
|
| 358 |
+
changeThresholdDefault: 0.1
|
| 359 |
+
}
|
| 360 |
+
},
|
| 361 |
+
entronaut: {
|
| 362 |
+
enabled: true,
|
| 363 |
+
sefaAnalysis: {
|
| 364 |
+
fieldSize: 1024,
|
| 365 |
+
fieldWidth: 32,
|
| 366 |
+
fieldHeight: 32,
|
| 367 |
+
updateFrequency: 3,
|
| 368 |
+
temporalSmoothing: 0.9,
|
| 369 |
+
intensitySmoothing: 0.1
|
| 370 |
+
},
|
| 371 |
+
weights: {
|
| 372 |
+
alpha_amplitude: 1.2,
|
| 373 |
+
alpha_phase: 0.8,
|
| 374 |
+
alpha_entropy: 1.5,
|
| 375 |
+
alpha_curvature: 1.0,
|
| 376 |
+
alpha_spectral: 1.0
|
| 377 |
+
},
|
| 378 |
+
adaptiveCoupling: {
|
| 379 |
+
enabled: true,
|
| 380 |
+
updateFrequency: 4,
|
| 381 |
+
dampingBase: 0.9,
|
| 382 |
+
dampingVariation: 0.3,
|
| 383 |
+
diffusionBase: 0.01,
|
| 384 |
+
diffusionVariation: 0.6,
|
| 385 |
+
couplingBase: 0.02,
|
| 386 |
+
couplingVariation: 0.04,
|
| 387 |
+
flockingRadius: 0.25,
|
| 388 |
+
flockingInfluence: 0.2,
|
| 389 |
+
metabolicPulseStrength: 0.5,
|
| 390 |
+
neighborCheckStep: 5,
|
| 391 |
+
neighborRange: 20
|
| 392 |
+
},
|
| 393 |
+
biomimeticBehavior: {
|
| 394 |
+
collectiveMemory: 0.9,
|
| 395 |
+
emergenceThreshold: 0.5,
|
| 396 |
+
coherenceWeight: 1.0,
|
| 397 |
+
complexityWeight: 1.0,
|
| 398 |
+
networkInfluence: 0.1,
|
| 399 |
+
syncPulseStrength: 0.2
|
| 400 |
+
}
|
| 401 |
+
},
|
| 402 |
+
tendrils: {
|
| 403 |
+
enabled: true,
|
| 404 |
+
maxTendrils: 2000,
|
| 405 |
+
defaultDensity: 0.3,
|
| 406 |
+
maxTendrilLength: 0.8,
|
| 407 |
+
tendrilSegments: 8,
|
| 408 |
+
updateInterval: 100,
|
| 409 |
+
sampleParticlesPerSphere: 10,
|
| 410 |
+
particleSampleStep: 1000,
|
| 411 |
+
curvature: {
|
| 412 |
+
midPointMultiplier: 0.2,
|
| 413 |
+
undulationStrength: 0.05,
|
| 414 |
+
timeFactorBase: 0.001,
|
| 415 |
+
timeFactorCurve: 0.0015,
|
| 416 |
+
spatialFrequency: 10
|
| 417 |
+
},
|
| 418 |
+
visual: {
|
| 419 |
+
baseOpacity: 0.3,
|
| 420 |
+
glowIntensity: 1.0,
|
| 421 |
+
colorIntensityBase: 0.3,
|
| 422 |
+
colorIntensityVariation: 0.7,
|
| 423 |
+
blendingMode: "additive"
|
| 424 |
+
}
|
| 425 |
+
},
|
| 426 |
+
cymatics: {
|
| 427 |
+
enabled: true,
|
| 428 |
+
intensity: 0.002,
|
| 429 |
+
patterns: {
|
| 430 |
+
radialWaves: { frequencyFactor: 0.1, petalCount: 6, amplitudeZ: 0.5 },
|
| 431 |
+
sphericalHarmonics: { lMultiplier: 0.5, mMultiplier: 0.3, timeMultiplier: 2 },
|
| 432 |
+
geometricLattice: { frequencyFactor: 0.2 },
|
| 433 |
+
flowerOfLife: { frequencyFactor: 0.15, basePetals: 6, petalVariation: 0.1, amplitudeZ: 0.3 }
|
| 434 |
+
},
|
| 435 |
+
breathing: { enabled: true, speed: 0.5, intensityBase: 0.3, intensityVariation: 0.7 }
|
| 436 |
+
},
|
| 437 |
+
colors: {
|
| 438 |
+
sphereColorSchemes: [
|
| 439 |
+
{ name: "sub-bass", start: "#ff3366", end: "#3366ff" },
|
| 440 |
+
{ name: "bass", start: "#ff6b35", end: "#c44569" },
|
| 441 |
+
{ name: "mid", start: "#f7931e", end: "#f8b500" },
|
| 442 |
+
{ name: "high-mid", start: "#ffe66d", end: "#feca57" },
|
| 443 |
+
{ name: "high", start: "#4ecdc4", end: "#45b7d1" }
|
| 444 |
+
],
|
| 445 |
+
tendrilColors: {
|
| 446 |
+
baseColor: [0.3, 0.3, 0.3],
|
| 447 |
+
frequencyColorMap: {
|
| 448 |
+
lowFreq: [0.8, 0.2, 0.2],
|
| 449 |
+
midFreq: [0.2, 0.8, 0.2],
|
| 450 |
+
highFreq: [0.2, 0.2, 0.8]
|
| 451 |
+
}
|
| 452 |
+
}
|
| 453 |
+
},
|
| 454 |
+
camera: {
|
| 455 |
+
defaultPosition: [0, 0, 2.5],
|
| 456 |
+
fov: 75,
|
| 457 |
+
near: 0.1,
|
| 458 |
+
far: 1000,
|
| 459 |
+
controls: {
|
| 460 |
+
enableDamping: true,
|
| 461 |
+
dampingFactor: 0.05,
|
| 462 |
+
maxDistance: 50,
|
| 463 |
+
minDistance: 0.1,
|
| 464 |
+
maxPolarAngle: 3.14159,
|
| 465 |
+
autoRotateSpeed: 0.5
|
| 466 |
+
},
|
| 467 |
+
presets: {
|
| 468 |
+
default: [0, 0, 2.5],
|
| 469 |
+
inside: [0, 0, 0.5],
|
| 470 |
+
far: [0, 0, 8],
|
| 471 |
+
top: [0, 5, 0],
|
| 472 |
+
side: [5, 0, 0]
|
| 473 |
+
}
|
| 474 |
+
},
|
| 475 |
+
fog: {
|
| 476 |
+
enabled: true,
|
| 477 |
+
color: "#000000",
|
| 478 |
+
near: 2.7,
|
| 479 |
+
far: 3.7,
|
| 480 |
+
density: 0.1
|
| 481 |
+
},
|
| 482 |
+
rendering: {
|
| 483 |
+
antialias: true,
|
| 484 |
+
background: "#000000",
|
| 485 |
+
particleBlending: "additive",
|
| 486 |
+
targetFPS: 60,
|
| 487 |
+
adaptiveQuality: {
|
| 488 |
+
enabled: true,
|
| 489 |
+
minFPS: 30,
|
| 490 |
+
qualityReductionStep: 0.1,
|
| 491 |
+
qualityRecoveryDelay: 2000
|
| 492 |
+
}
|
| 493 |
+
},
|
| 494 |
+
ui: {
|
| 495 |
+
controls: {
|
| 496 |
+
defaultCollapsed: true,
|
| 497 |
+
position: "bottom-right",
|
| 498 |
+
maxWidth: "250px",
|
| 499 |
+
opacity: 0.8
|
| 500 |
+
},
|
| 501 |
+
about: {
|
| 502 |
+
defaultCollapsed: true,
|
| 503 |
+
position: "bottom-center",
|
| 504 |
+
width: "350px"
|
| 505 |
+
},
|
| 506 |
+
animation: {
|
| 507 |
+
transitionDuration: "0.3s",
|
| 508 |
+
hoverScale: 1.1,
|
| 509 |
+
backdropBlur: "10px"
|
| 510 |
+
}
|
| 511 |
+
},
|
| 512 |
+
performance: {
|
| 513 |
+
adaptiveSettings: {
|
| 514 |
+
enabled: true,
|
| 515 |
+
thresholds: {
|
| 516 |
+
particleReduction: 25,
|
| 517 |
+
tendrilReduction: 20,
|
| 518 |
+
sefaDisable: 15,
|
| 519 |
+
couplingDisable: 10
|
| 520 |
+
}
|
| 521 |
+
},
|
| 522 |
+
optimizations: {
|
| 523 |
+
particleUpdateStep: 1,
|
| 524 |
+
tendrilUpdateStep: 4,
|
| 525 |
+
sefaUpdateStep: 10,
|
| 526 |
+
couplingUpdateStep: 4,
|
| 527 |
+
neighborCheckReduction: 5
|
| 528 |
+
}
|
| 529 |
+
},
|
| 530 |
+
presets: {}
|
| 531 |
+
}
|
| 532 |
+
}
|
| 533 |
+
|
| 534 |
+
applyPreset(presetName: string): void {
|
| 535 |
+
if (!this.config) {
|
| 536 |
+
console.warn('Config not loaded, cannot apply preset')
|
| 537 |
+
return
|
| 538 |
+
}
|
| 539 |
+
|
| 540 |
+
const preset = this.config.presets[presetName]
|
| 541 |
+
if (!preset) {
|
| 542 |
+
console.warn(`Preset '${presetName}' not found`)
|
| 543 |
+
return
|
| 544 |
+
}
|
| 545 |
+
|
| 546 |
+
// Deep merge preset into current config
|
| 547 |
+
this.config = this.deepMerge(this.config, preset)
|
| 548 |
+
console.log(`✅ Applied preset: ${presetName}`)
|
| 549 |
+
}
|
| 550 |
+
|
| 551 |
+
private deepMerge(target: any, source: any): any {
|
| 552 |
+
const result = { ...target }
|
| 553 |
+
|
| 554 |
+
for (const key in source) {
|
| 555 |
+
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
| 556 |
+
result[key] = this.deepMerge(target[key] || {}, source[key])
|
| 557 |
+
} else {
|
| 558 |
+
result[key] = source[key]
|
| 559 |
+
}
|
| 560 |
+
}
|
| 561 |
+
|
| 562 |
+
return result
|
| 563 |
+
}
|
| 564 |
+
|
| 565 |
+
getConfig(): VisualizerConfig | null {
|
| 566 |
+
return this.config
|
| 567 |
+
}
|
| 568 |
+
|
| 569 |
+
// Helper methods to get specific config sections
|
| 570 |
+
getParticleParams(sphereIndex: number): Partial<ParticleSystemParams> {
|
| 571 |
+
if (!this.config) return {}
|
| 572 |
+
|
| 573 |
+
const audioRange = this.config.audio.frequencyRanges[sphereIndex] || this.config.audio.frequencyRanges[0]
|
| 574 |
+
const colors = this.config.colors.sphereColorSchemes[sphereIndex] || this.config.colors.sphereColorSchemes[0]
|
| 575 |
+
|
| 576 |
+
return {
|
| 577 |
+
enabled: true,
|
| 578 |
+
particleCount: this.config.visualization.particles.defaultParticleCount,
|
| 579 |
+
particleSize: this.config.visualization.particles.particleSize,
|
| 580 |
+
particleLifetime: this.config.visualization.particles.particleLifetime,
|
| 581 |
+
sphereRadius: this.config.visualization.spheres.baseRadius + (sphereIndex * this.config.visualization.spheres.radiusIncrement),
|
| 582 |
+
innerSphereRadius: this.config.visualization.spheres.innerSphereRadius,
|
| 583 |
+
turbulenceStrength: this.config.visualization.physics.turbulenceStrength,
|
| 584 |
+
noiseScale: this.config.visualization.physics.noiseScale + (sphereIndex * this.config.visualization.physics.noiseScaleVariation),
|
| 585 |
+
noiseSpeed: this.config.visualization.physics.noiseSpeed,
|
| 586 |
+
rotationSpeedMin: this.config.visualization.rotation.rotationSpeedMin,
|
| 587 |
+
rotationSpeedMax: this.config.visualization.rotation.rotationSpeedMax + (sphereIndex * this.config.visualization.rotation.rotationSpeedIncrement),
|
| 588 |
+
rotationSmoothness: this.config.visualization.rotation.rotationSmoothness,
|
| 589 |
+
beatStrength: this.config.audio.beatDetection.beatStrength,
|
| 590 |
+
beatThreshold: audioRange.beatThreshold,
|
| 591 |
+
volumeChangeThreshold: audioRange.volumeChangeThreshold,
|
| 592 |
+
minFrequency: audioRange.minFrequency,
|
| 593 |
+
maxFrequency: audioRange.maxFrequency,
|
| 594 |
+
colorStart: colors.start,
|
| 595 |
+
colorEnd: colors.end,
|
| 596 |
+
dynamicNoiseScale: true
|
| 597 |
+
}
|
| 598 |
+
}
|
| 599 |
+
|
| 600 |
+
getCymateIntensity(): number {
|
| 601 |
+
return this.config?.cymatics.intensity || 0.002
|
| 602 |
+
}
|
| 603 |
+
|
| 604 |
+
getTendrilConfig(): any {
|
| 605 |
+
return this.config?.tendrils || {}
|
| 606 |
+
}
|
| 607 |
+
|
| 608 |
+
getEntronautConfig(): any {
|
| 609 |
+
return this.config?.entronaut || {}
|
| 610 |
+
}
|
| 611 |
+
|
| 612 |
+
getFogConfig(): any {
|
| 613 |
+
return this.config?.fog || {}
|
| 614 |
+
}
|
| 615 |
+
|
| 616 |
+
getCameraConfig(): any {
|
| 617 |
+
return this.config?.camera || {}
|
| 618 |
+
}
|
| 619 |
+
|
| 620 |
+
// Hot reload support for development
|
| 621 |
+
async reloadConfig(): Promise<VisualizerConfig> {
|
| 622 |
+
this.config = null
|
| 623 |
+
this.loadPromise = null
|
| 624 |
+
return this.loadConfig()
|
| 625 |
+
}
|
| 626 |
+
}
|
| 627 |
+
|
| 628 |
+
// Singleton instance
|
| 629 |
+
export const configLoader = new ConfigLoader()
|
| 630 |
+
|
| 631 |
+
// Export for use in components
|
| 632 |
+
export default configLoader
|
src/utils/entronaut.ts
ADDED
|
@@ -0,0 +1,503 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Entronaut: Symbolic Emergence Field Analysis for Audio-Reactive Particle Systems
|
| 2 |
+
// Implementation of the advanced algorithmic framework for emergent structure detection
|
| 3 |
+
|
| 4 |
+
import { AudioEngine } from '../engine/AudioEngine'
|
| 5 |
+
|
| 6 |
+
// Core SEFA Types
|
| 7 |
+
export interface SEFAFeatures {
|
| 8 |
+
amplitude: Float32Array // A(y) - envelope amplitude
|
| 9 |
+
instantaneousPhase: Float32Array // φ(y) - unwrapped phase
|
| 10 |
+
instantaneousFreq: Float32Array // F(y) - local frequency
|
| 11 |
+
curvature: Float32Array // C(y) - amplitude curvature
|
| 12 |
+
entropy: Float32Array // S(y) - local sliding window entropy
|
| 13 |
+
entropyAlignment: Float32Array // E(y) - normalized entropy (1 - S/S_max)
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
export interface SEFAScore {
|
| 17 |
+
sefa: Float32Array // Combined SEFA score
|
| 18 |
+
features: SEFAFeatures // Individual feature maps
|
| 19 |
+
alphas: { // Information deficit weights
|
| 20 |
+
amplitude: number
|
| 21 |
+
curvature: number
|
| 22 |
+
frequency: number
|
| 23 |
+
entropy: number
|
| 24 |
+
}
|
| 25 |
+
maxValues: { // Normalization maxima
|
| 26 |
+
amplitude: number
|
| 27 |
+
curvature: number
|
| 28 |
+
frequency: number
|
| 29 |
+
entropy: number
|
| 30 |
+
}
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
export interface FieldState {
|
| 34 |
+
phi: Float32Array // Primary field φ(x,t)
|
| 35 |
+
velocity: Float32Array // ∂φ/∂t
|
| 36 |
+
width: number
|
| 37 |
+
height: number
|
| 38 |
+
deltaX: number
|
| 39 |
+
deltaT: number
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
export interface FieldParams {
|
| 43 |
+
rho: number // Inertial coefficient
|
| 44 |
+
gamma: number // Damping coefficient
|
| 45 |
+
D: number // Diffusion coefficient
|
| 46 |
+
potential: (phi: number) => number // V'(φ) potential force
|
| 47 |
+
potentialDerivative: (phi: number) => number
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
export interface CouplingParams {
|
| 51 |
+
strengthGamma: number // How much SEFA affects damping
|
| 52 |
+
strengthD: number // How much SEFA affects diffusion
|
| 53 |
+
strengthV: number // How much SEFA affects potential
|
| 54 |
+
smoothingKernel: number // Spatial smoothing for stability
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
export class EntronautEngine {
|
| 58 |
+
private sefaWindowSize: number = 64
|
| 59 |
+
private entropyBins: number = 32
|
| 60 |
+
private epsilon: number = 1e-16
|
| 61 |
+
|
| 62 |
+
constructor() {
|
| 63 |
+
// Initialize Entronaut engine
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
// SEFA Core: Analyze 1D signal for symbolic emergence
|
| 67 |
+
public analyzeSEFA(signal: Float32Array): SEFAScore {
|
| 68 |
+
if (signal.length < this.sefaWindowSize) {
|
| 69 |
+
throw new Error('Signal too short for SEFA analysis')
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
// Step 1: Compute analytic signal using Hilbert transform
|
| 73 |
+
const analyticSignal = this.computeAnalyticSignal(signal)
|
| 74 |
+
|
| 75 |
+
// Step 2: Extract raw features
|
| 76 |
+
const features = this.extractSEFAFeatures(signal, analyticSignal)
|
| 77 |
+
|
| 78 |
+
// Step 3: Normalize features (max-absolute scaling)
|
| 79 |
+
const { normalizedFeatures, maxValues } = this.normalizeFeatures(features)
|
| 80 |
+
|
| 81 |
+
// Step 4: Compute information deficit weights (alphas)
|
| 82 |
+
const alphas = this.computeInformationDeficitWeights(normalizedFeatures)
|
| 83 |
+
|
| 84 |
+
// Step 5: Calculate final SEFA score using weighted geometric mean
|
| 85 |
+
const sefa = this.computeSEFAScore(normalizedFeatures, alphas)
|
| 86 |
+
|
| 87 |
+
return {
|
| 88 |
+
sefa,
|
| 89 |
+
features: normalizedFeatures,
|
| 90 |
+
alphas,
|
| 91 |
+
maxValues
|
| 92 |
+
}
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
// Hilbert Transform for analytic signal
|
| 96 |
+
private computeAnalyticSignal(signal: Float32Array): { real: Float32Array, imag: Float32Array } {
|
| 97 |
+
const N = signal.length
|
| 98 |
+
const real = new Float32Array(signal)
|
| 99 |
+
const imag = new Float32Array(N)
|
| 100 |
+
|
| 101 |
+
// FFT-based Hilbert transform
|
| 102 |
+
// This is a simplified implementation - in production would use proper FFT library
|
| 103 |
+
for (let i = 0; i < N; i++) {
|
| 104 |
+
let sum = 0
|
| 105 |
+
for (let j = 0; j < N; j++) {
|
| 106 |
+
if (i !== j) {
|
| 107 |
+
// Discrete Hilbert transform kernel approximation
|
| 108 |
+
const weight = 1.0 / (Math.PI * (i - j))
|
| 109 |
+
sum += signal[j] * weight
|
| 110 |
+
}
|
| 111 |
+
}
|
| 112 |
+
imag[i] = sum
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
return { real, imag }
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
// Extract all SEFA features from analytic signal
|
| 119 |
+
private extractSEFAFeatures(signal: Float32Array, analytic: { real: Float32Array, imag: Float32Array }): SEFAFeatures {
|
| 120 |
+
const N = signal.length
|
| 121 |
+
|
| 122 |
+
// 1. Amplitude A(y) = |V_A(y)|
|
| 123 |
+
const amplitude = new Array(N)
|
| 124 |
+
for (let i = 0; i < N; i++) {
|
| 125 |
+
amplitude[i] = Math.sqrt(analytic.real[i] * analytic.real[i] + analytic.imag[i] * analytic.imag[i])
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
// 2. Instantaneous Phase φ(y) = arg(V_A(y)) - unwrapped
|
| 129 |
+
const instantaneousPhase = new Array(N)
|
| 130 |
+
for (let i = 0; i < N; i++) {
|
| 131 |
+
instantaneousPhase[i] = Math.atan2(analytic.imag[i], analytic.real[i])
|
| 132 |
+
}
|
| 133 |
+
this.unwrapPhase(instantaneousPhase)
|
| 134 |
+
|
| 135 |
+
// 3. Instantaneous Frequency F(y) = dφ/dy (with smoothing for stability)
|
| 136 |
+
const smoothedPhase = this.smoothArray(instantaneousPhase, 3)
|
| 137 |
+
const instantaneousFreq = this.computeDerivative(smoothedPhase)
|
| 138 |
+
|
| 139 |
+
// 4. Curvature C(y) = d²A/dy² (with smoothing for stability)
|
| 140 |
+
const smoothedAmplitude = this.smoothArray(amplitude, 3)
|
| 141 |
+
const amplitudeDerivative = this.computeDerivative(smoothedAmplitude)
|
| 142 |
+
const curvature = this.computeDerivative(amplitudeDerivative)
|
| 143 |
+
|
| 144 |
+
// 5. Local Sliding Window Entropy S(y)
|
| 145 |
+
const entropy = this.computeLocalEntropy(signal)
|
| 146 |
+
|
| 147 |
+
// 6. Entropy Alignment E(y) = 1 - S(y)/S_max
|
| 148 |
+
const maxEntropy = Math.max(...Array.from(entropy))
|
| 149 |
+
const entropyAlignment = new Float32Array(entropy.length)
|
| 150 |
+
for (let i = 0; i < entropy.length; i++) {
|
| 151 |
+
entropyAlignment[i] = maxEntropy > 0 ? 1 - entropy[i] / maxEntropy : 0
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
return {
|
| 155 |
+
amplitude: Float32Array.from(amplitude),
|
| 156 |
+
instantaneousPhase: Float32Array.from(instantaneousPhase),
|
| 157 |
+
instantaneousFreq: Float32Array.from(instantaneousFreq),
|
| 158 |
+
curvature: Float32Array.from(curvature),
|
| 159 |
+
entropy: Float32Array.from(entropy),
|
| 160 |
+
entropyAlignment: Float32Array.from(entropyAlignment)
|
| 161 |
+
}
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
// Phase unwrapping to ensure continuity
|
| 165 |
+
private unwrapPhase(phase: number[]): void {
|
| 166 |
+
for (let i = 1; i < phase.length; i++) {
|
| 167 |
+
let diff = phase[i] - phase[i-1]
|
| 168 |
+
if (diff > Math.PI) {
|
| 169 |
+
phase[i] -= 2 * Math.PI
|
| 170 |
+
} else if (diff < -Math.PI) {
|
| 171 |
+
phase[i] += 2 * Math.PI
|
| 172 |
+
}
|
| 173 |
+
}
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
// Compute local entropy using sliding window
|
| 177 |
+
private computeLocalEntropy(signal: Float32Array): Float32Array {
|
| 178 |
+
const N = signal.length
|
| 179 |
+
const entropy = new Float32Array(N)
|
| 180 |
+
const halfWindow = Math.floor(this.sefaWindowSize / 2)
|
| 181 |
+
|
| 182 |
+
for (let i = 0; i < N; i++) {
|
| 183 |
+
const start = Math.max(0, i - halfWindow)
|
| 184 |
+
const end = Math.min(N, i + halfWindow)
|
| 185 |
+
const windowData = signal.slice(start, end)
|
| 186 |
+
|
| 187 |
+
entropy[i] = this.computeHistogramEntropy(windowData)
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
return entropy
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
// Shannon entropy from histogram
|
| 194 |
+
private computeHistogramEntropy(data: Float32Array): number {
|
| 195 |
+
if (data.length === 0) return 0
|
| 196 |
+
|
| 197 |
+
// Adaptive binning using Freedman-Diaconis rule
|
| 198 |
+
const sortedData = Array.from(data).sort((a, b) => a - b)
|
| 199 |
+
const q75 = this.percentile(sortedData, 0.75)
|
| 200 |
+
const q25 = this.percentile(sortedData, 0.25)
|
| 201 |
+
const iqr = q75 - q25
|
| 202 |
+
const binWidth = 2 * iqr * Math.pow(data.length, -1/3)
|
| 203 |
+
const numBins = Math.max(1, Math.min(this.entropyBins, Math.ceil((sortedData[sortedData.length-1] - sortedData[0]) / binWidth)))
|
| 204 |
+
|
| 205 |
+
// Create histogram
|
| 206 |
+
const min = sortedData[0]
|
| 207 |
+
const max = sortedData[sortedData.length - 1]
|
| 208 |
+
const range = max - min
|
| 209 |
+
const bins = new Array(numBins).fill(0)
|
| 210 |
+
|
| 211 |
+
for (const value of data) {
|
| 212 |
+
let binIndex = Math.floor(((value - min) / range) * (numBins - 1))
|
| 213 |
+
binIndex = Math.max(0, Math.min(numBins - 1, binIndex))
|
| 214 |
+
bins[binIndex]++
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
// Compute Shannon entropy
|
| 218 |
+
let entropy = 0
|
| 219 |
+
for (const count of bins) {
|
| 220 |
+
if (count > 0) {
|
| 221 |
+
const prob = count / data.length
|
| 222 |
+
entropy -= prob * Math.log(prob)
|
| 223 |
+
}
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
return entropy
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
// Information deficit weighting calculation
|
| 230 |
+
private computeInformationDeficitWeights(features: SEFAFeatures): { amplitude: number, curvature: number, frequency: number, entropy: number } {
|
| 231 |
+
const featureArrays = [
|
| 232 |
+
features.amplitude,
|
| 233 |
+
features.curvature,
|
| 234 |
+
features.instantaneousFreq,
|
| 235 |
+
features.entropyAlignment
|
| 236 |
+
]
|
| 237 |
+
|
| 238 |
+
const featureNames = ['amplitude', 'curvature', 'frequency', 'entropy'] as const
|
| 239 |
+
const weights = {} as any
|
| 240 |
+
let totalWeight = 0
|
| 241 |
+
|
| 242 |
+
// Compute global entropy for each feature
|
| 243 |
+
for (let i = 0; i < featureArrays.length; i++) {
|
| 244 |
+
const globalEntropy = this.computeHistogramEntropy(featureArrays[i])
|
| 245 |
+
const maxPossibleEntropy = Math.log(this.entropyBins)
|
| 246 |
+
const informationDeficit = Math.max(0, maxPossibleEntropy - globalEntropy)
|
| 247 |
+
|
| 248 |
+
weights[featureNames[i]] = informationDeficit
|
| 249 |
+
totalWeight += informationDeficit
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
// Normalize weights so they sum to 4 (number of features)
|
| 253 |
+
if (totalWeight > 0) {
|
| 254 |
+
for (const name of featureNames) {
|
| 255 |
+
weights[name] = (4 * weights[name]) / totalWeight
|
| 256 |
+
}
|
| 257 |
+
} else {
|
| 258 |
+
// Default equal weighting if all features are random
|
| 259 |
+
for (const name of featureNames) {
|
| 260 |
+
weights[name] = 1
|
| 261 |
+
}
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
return weights
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
// Weighted geometric mean SEFA score
|
| 268 |
+
private computeSEFAScore(features: SEFAFeatures, alphas: any): Float32Array {
|
| 269 |
+
const N = features.amplitude.length
|
| 270 |
+
const sefa = new Float32Array(N)
|
| 271 |
+
|
| 272 |
+
for (let i = 0; i < N; i++) {
|
| 273 |
+
// Regularize features to prevent log(0)
|
| 274 |
+
const regA = Math.max(this.epsilon, Math.abs(features.amplitude[i]))
|
| 275 |
+
const regC = Math.max(this.epsilon, Math.abs(features.curvature[i]))
|
| 276 |
+
const regF = Math.max(this.epsilon, Math.abs(features.instantaneousFreq[i]))
|
| 277 |
+
const regE = Math.max(this.epsilon, Math.abs(features.entropyAlignment[i]))
|
| 278 |
+
|
| 279 |
+
// Compute log-SEFA score
|
| 280 |
+
const logSEFA =
|
| 281 |
+
alphas.amplitude * Math.log(regA) +
|
| 282 |
+
alphas.curvature * Math.log(regC) +
|
| 283 |
+
alphas.frequency * Math.log(regF) +
|
| 284 |
+
alphas.entropy * Math.log(regE)
|
| 285 |
+
|
| 286 |
+
// Convert back to linear scale
|
| 287 |
+
sefa[i] = Math.exp(logSEFA)
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
return sefa
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
// Field Evolution: Solve generalized PDE φ_tt + γφ_t - D∇²φ + V'(φ) = 0
|
| 294 |
+
public evolveField(
|
| 295 |
+
fieldState: FieldState,
|
| 296 |
+
params: FieldParams,
|
| 297 |
+
coupling: CouplingParams,
|
| 298 |
+
sefaScore?: SEFAScore
|
| 299 |
+
): FieldState {
|
| 300 |
+
const { phi, velocity, width, height, deltaT } = fieldState
|
| 301 |
+
const N = phi.length
|
| 302 |
+
|
| 303 |
+
// Create new field state
|
| 304 |
+
const newPhi = new Float32Array(N)
|
| 305 |
+
const newVelocity = new Float32Array(N)
|
| 306 |
+
|
| 307 |
+
// Compute effective parameters from SEFA coupling (if available)
|
| 308 |
+
let gammaEff = new Float32Array(N).fill(params.gamma)
|
| 309 |
+
let DEff = new Float32Array(N).fill(params.D)
|
| 310 |
+
|
| 311 |
+
if (sefaScore && coupling) {
|
| 312 |
+
this.applySEFACoupling(gammaEff, DEff, sefaScore, coupling)
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
// Solve PDE using semi-implicit scheme for stability
|
| 316 |
+
for (let i = 0; i < height; i++) {
|
| 317 |
+
for (let j = 0; j < width; j++) {
|
| 318 |
+
const idx = i * width + j
|
| 319 |
+
|
| 320 |
+
// Laplacian (diffusion term) using 5-point stencil
|
| 321 |
+
const laplacian = this.computeLaplacian(phi, i, j, width, height, fieldState.deltaX)
|
| 322 |
+
|
| 323 |
+
// Potential force
|
| 324 |
+
const potentialForce = -params.potentialDerivative(phi[idx])
|
| 325 |
+
|
| 326 |
+
// PDE: ρφ_tt + γφ_t - D∇²φ + V'(φ) = 0
|
| 327 |
+
// Rearranged: φ_tt = (D∇²φ - γφ_t - V'(φ))/ρ
|
| 328 |
+
const acceleration = (DEff[idx] * laplacian - gammaEff[idx] * velocity[idx] + potentialForce) / params.rho
|
| 329 |
+
|
| 330 |
+
// Semi-implicit time stepping for stability
|
| 331 |
+
newVelocity[idx] = velocity[idx] + acceleration * deltaT
|
| 332 |
+
newPhi[idx] = phi[idx] + newVelocity[idx] * deltaT
|
| 333 |
+
|
| 334 |
+
// Apply boundary conditions (periodic)
|
| 335 |
+
if (newPhi[idx] > 2.0) newPhi[idx] = 2.0
|
| 336 |
+
if (newPhi[idx] < -2.0) newPhi[idx] = -2.0
|
| 337 |
+
}
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
return {
|
| 341 |
+
...fieldState,
|
| 342 |
+
phi: newPhi,
|
| 343 |
+
velocity: newVelocity
|
| 344 |
+
}
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
// Apply SEFA-based coupling to modify field parameters
|
| 348 |
+
private applySEFACoupling(
|
| 349 |
+
gammaEff: Float32Array,
|
| 350 |
+
DEff: Float32Array,
|
| 351 |
+
sefaScore: SEFAScore,
|
| 352 |
+
coupling: CouplingParams
|
| 353 |
+
): void {
|
| 354 |
+
// Smooth SEFA scores for numerical stability
|
| 355 |
+
const smoothedSEFA = this.spatialSmooth(sefaScore.sefa, coupling.smoothingKernel)
|
| 356 |
+
const smoothedEntropy = this.spatialSmooth(sefaScore.features.entropyAlignment, coupling.smoothingKernel)
|
| 357 |
+
|
| 358 |
+
for (let i = 0; i < gammaEff.length; i++) {
|
| 359 |
+
const sefa = smoothedSEFA[i]
|
| 360 |
+
const entropy = smoothedEntropy[i]
|
| 361 |
+
|
| 362 |
+
// High SEFA + high entropy alignment = reduce damping (let structure persist)
|
| 363 |
+
const dampingModifier = 1.0 - coupling.strengthGamma * sefa * entropy
|
| 364 |
+
gammaEff[i] *= Math.max(0.1, Math.min(2.0, dampingModifier))
|
| 365 |
+
|
| 366 |
+
// High SEFA = reduce diffusion (create boundaries)
|
| 367 |
+
const diffusionModifier = 1.0 - coupling.strengthD * sefa
|
| 368 |
+
DEff[i] *= Math.max(0.01, Math.min(1.0, diffusionModifier))
|
| 369 |
+
}
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
// Audio Integration: Convert audio frequency data to field
|
| 373 |
+
public audioToField(audioEngine: AudioEngine, sphere: any): FieldState {
|
| 374 |
+
const audioData = audioEngine.getAdvancedAudioData(sphere)
|
| 375 |
+
|
| 376 |
+
// Create field from frequency spectrum - using 32x32 grid for performance
|
| 377 |
+
const width = 32
|
| 378 |
+
const height = 32
|
| 379 |
+
const N = width * height
|
| 380 |
+
|
| 381 |
+
const phi = new Float32Array(N)
|
| 382 |
+
const velocity = new Float32Array(N)
|
| 383 |
+
|
| 384 |
+
// Map audio data to 2D field with cymatic patterns
|
| 385 |
+
for (let i = 0; i < height; i++) {
|
| 386 |
+
for (let j = 0; j < width; j++) {
|
| 387 |
+
const idx = i * width + j
|
| 388 |
+
|
| 389 |
+
// Create cymatic-inspired patterns
|
| 390 |
+
const r = Math.sqrt((i - height/2)**2 + (j - width/2)**2) / (width/2)
|
| 391 |
+
const theta = Math.atan2(i - height/2, j - width/2)
|
| 392 |
+
|
| 393 |
+
// Combine frequency data with spatial patterns
|
| 394 |
+
const freqValue = audioData.rangeEnergy
|
| 395 |
+
const beatStrength = audioData.rangeEnergyBeat
|
| 396 |
+
const spatialPattern = Math.sin(r * 10 + beatStrength * 5) * Math.cos(theta * 6)
|
| 397 |
+
|
| 398 |
+
phi[idx] = freqValue * spatialPattern
|
| 399 |
+
velocity[idx] = 0
|
| 400 |
+
}
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
return {
|
| 404 |
+
phi,
|
| 405 |
+
velocity,
|
| 406 |
+
width,
|
| 407 |
+
height,
|
| 408 |
+
deltaX: 1.0 / width,
|
| 409 |
+
deltaT: 1.0 / 60.0 // 60 FPS
|
| 410 |
+
}
|
| 411 |
+
}
|
| 412 |
+
|
| 413 |
+
// Helper functions
|
| 414 |
+
private computeLaplacian(field: Float32Array, i: number, j: number, width: number, height: number, deltaX: number): number {
|
| 415 |
+
const idx = i * width + j
|
| 416 |
+
const up = ((i - 1 + height) % height) * width + j
|
| 417 |
+
const down = ((i + 1) % height) * width + j
|
| 418 |
+
const left = i * width + ((j - 1 + width) % width)
|
| 419 |
+
const right = i * width + ((j + 1) % width)
|
| 420 |
+
|
| 421 |
+
return (field[up] + field[down] + field[left] + field[right] - 4 * field[idx]) / (deltaX * deltaX)
|
| 422 |
+
}
|
| 423 |
+
|
| 424 |
+
private smoothArray(arr: number[] | Float32Array, kernelSize: number): Float32Array {
|
| 425 |
+
const result = new Float32Array(arr.length)
|
| 426 |
+
const half = Math.floor(kernelSize / 2)
|
| 427 |
+
|
| 428 |
+
for (let i = 0; i < arr.length; i++) {
|
| 429 |
+
let sum = 0
|
| 430 |
+
let count = 0
|
| 431 |
+
|
| 432 |
+
for (let j = -half; j <= half; j++) {
|
| 433 |
+
const idx = i + j
|
| 434 |
+
if (idx >= 0 && idx < arr.length) {
|
| 435 |
+
sum += arr[idx]
|
| 436 |
+
count++
|
| 437 |
+
}
|
| 438 |
+
}
|
| 439 |
+
|
| 440 |
+
result[i] = count > 0 ? sum / count : arr[i]
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
return result
|
| 444 |
+
}
|
| 445 |
+
|
| 446 |
+
private computeDerivative(arr: number[] | Float32Array): Float32Array {
|
| 447 |
+
const result = new Float32Array(arr.length)
|
| 448 |
+
|
| 449 |
+
for (let i = 1; i < arr.length - 1; i++) {
|
| 450 |
+
result[i] = (arr[i + 1] - arr[i - 1]) / 2
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
// Handle boundaries
|
| 454 |
+
result[0] = arr[1] - arr[0]
|
| 455 |
+
result[arr.length - 1] = arr[arr.length - 1] - arr[arr.length - 2]
|
| 456 |
+
|
| 457 |
+
return result
|
| 458 |
+
}
|
| 459 |
+
|
| 460 |
+
private normalizeFeatures(features: SEFAFeatures): { normalizedFeatures: SEFAFeatures, maxValues: any } {
|
| 461 |
+
const maxAmplitude = Math.max(...features.amplitude)
|
| 462 |
+
const maxCurvature = Math.max(...features.curvature.map(Math.abs))
|
| 463 |
+
const maxFrequency = Math.max(...features.instantaneousFreq.map(Math.abs))
|
| 464 |
+
const maxEntropy = Math.max(...features.entropyAlignment)
|
| 465 |
+
|
| 466 |
+
return {
|
| 467 |
+
normalizedFeatures: {
|
| 468 |
+
amplitude: features.amplitude.map(x => maxAmplitude > 0 ? x / maxAmplitude : 0),
|
| 469 |
+
curvature: features.curvature.map(x => maxCurvature > 0 ? x / maxCurvature : 0),
|
| 470 |
+
instantaneousFreq: features.instantaneousFreq.map(x => maxFrequency > 0 ? x / maxFrequency : 0),
|
| 471 |
+
instantaneousPhase: features.instantaneousPhase,
|
| 472 |
+
entropy: features.entropy,
|
| 473 |
+
entropyAlignment: features.entropyAlignment.map(x => maxEntropy > 0 ? x / maxEntropy : 0)
|
| 474 |
+
},
|
| 475 |
+
maxValues: {
|
| 476 |
+
amplitude: maxAmplitude,
|
| 477 |
+
curvature: maxCurvature,
|
| 478 |
+
frequency: maxFrequency,
|
| 479 |
+
entropy: maxEntropy
|
| 480 |
+
}
|
| 481 |
+
}
|
| 482 |
+
}
|
| 483 |
+
|
| 484 |
+
|
| 485 |
+
|
| 486 |
+
private spatialSmooth(data: Float32Array, _kernel: number): Float32Array {
|
| 487 |
+
// Simplified 1D smoothing - in full implementation would use 2D convolution
|
| 488 |
+
return this.smoothArray(data, 5)
|
| 489 |
+
}
|
| 490 |
+
|
| 491 |
+
private percentile(sortedArray: number[], p: number): number {
|
| 492 |
+
const index = p * (sortedArray.length - 1)
|
| 493 |
+
const lower = Math.floor(index)
|
| 494 |
+
const upper = Math.ceil(index)
|
| 495 |
+
|
| 496 |
+
if (lower === upper) {
|
| 497 |
+
return sortedArray[lower]
|
| 498 |
+
}
|
| 499 |
+
|
| 500 |
+
const weight = index - lower
|
| 501 |
+
return sortedArray[lower] * (1 - weight) + sortedArray[upper] * weight
|
| 502 |
+
}
|
| 503 |
+
}
|
src/utils/noise.ts
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Enhanced 3D Perlin Noise Generator
|
| 2 |
+
export class NoiseGenerator {
|
| 3 |
+
private p: number[] = new Array(256).fill(0).map((_, i) => i)
|
| 4 |
+
private perm: number[] = new Array(512)
|
| 5 |
+
|
| 6 |
+
constructor() {
|
| 7 |
+
this.init()
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
private init() {
|
| 11 |
+
// Shuffle the permutation array
|
| 12 |
+
for (let i = this.p.length - 1; i > 0; i--) {
|
| 13 |
+
const j = Math.floor(Math.random() * (i + 1))
|
| 14 |
+
;[this.p[i], this.p[j]] = [this.p[j], this.p[i]]
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
// Create extended permutation array
|
| 18 |
+
for (let i = 0; i < 512; i++) {
|
| 19 |
+
this.perm[i] = this.p[i & 255]
|
| 20 |
+
}
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
private fade(t: number): number {
|
| 24 |
+
return t * t * t * (t * (t * 6 - 15) + 10)
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
private lerp(t: number, a: number, b: number): number {
|
| 28 |
+
return a + t * (b - a)
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
private grad(hash: number, x: number, y: number, z: number): number {
|
| 32 |
+
const h = hash & 15
|
| 33 |
+
const u = h < 8 ? x : y
|
| 34 |
+
const v = h < 4 ? y : h == 12 || h == 14 ? x : z
|
| 35 |
+
return ((h & 1) == 0 ? u : -u) + ((h & 2) == 0 ? v : -v)
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
public noise3D(x: number, y: number, z: number): number {
|
| 39 |
+
const X = Math.floor(x) & 255
|
| 40 |
+
const Y = Math.floor(y) & 255
|
| 41 |
+
const Z = Math.floor(z) & 255
|
| 42 |
+
|
| 43 |
+
x -= Math.floor(x)
|
| 44 |
+
y -= Math.floor(y)
|
| 45 |
+
z -= Math.floor(z)
|
| 46 |
+
|
| 47 |
+
const u = this.fade(x)
|
| 48 |
+
const v = this.fade(y)
|
| 49 |
+
const w = this.fade(z)
|
| 50 |
+
|
| 51 |
+
const A = this.perm[X] + Y
|
| 52 |
+
const AA = this.perm[A] + Z
|
| 53 |
+
const AB = this.perm[A + 1] + Z
|
| 54 |
+
const B = this.perm[X + 1] + Y
|
| 55 |
+
const BA = this.perm[B] + Z
|
| 56 |
+
const BB = this.perm[B + 1] + Z
|
| 57 |
+
|
| 58 |
+
return this.lerp(w,
|
| 59 |
+
this.lerp(v,
|
| 60 |
+
this.lerp(u, this.grad(this.perm[AA], x, y, z),
|
| 61 |
+
this.grad(this.perm[BA], x-1, y, z)),
|
| 62 |
+
this.lerp(u, this.grad(this.perm[AB], x, y-1, z),
|
| 63 |
+
this.grad(this.perm[BB], x-1, y-1, z))),
|
| 64 |
+
this.lerp(v,
|
| 65 |
+
this.lerp(u, this.grad(this.perm[AA+1], x, y, z-1),
|
| 66 |
+
this.grad(this.perm[BA+1], x-1, y, z-1)),
|
| 67 |
+
this.lerp(u, this.grad(this.perm[AB+1], x, y-1, z-1),
|
| 68 |
+
this.grad(this.perm[BB+1], x-1, y-1, z-1))))
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
// 2D noise for simpler use cases
|
| 72 |
+
noise2D(x: number, y: number): number {
|
| 73 |
+
return this.noise3D(x, y, 0)
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
// Generate fractal noise with multiple octaves
|
| 77 |
+
fractalNoise(x: number, y: number, z: number, octaves: number = 4, persistence: number = 0.5): number {
|
| 78 |
+
let value = 0
|
| 79 |
+
let amplitude = 1
|
| 80 |
+
let frequency = 1
|
| 81 |
+
let maxValue = 0
|
| 82 |
+
|
| 83 |
+
for (let i = 0; i < octaves; i++) {
|
| 84 |
+
value += this.noise3D(x * frequency, y * frequency, z * frequency) * amplitude
|
| 85 |
+
maxValue += amplitude
|
| 86 |
+
amplitude *= persistence
|
| 87 |
+
frequency *= 2
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
return value / maxValue
|
| 91 |
+
}
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
// Beat Detection and Wave Manager
|
| 95 |
+
export class BeatManager {
|
| 96 |
+
public currentWaveRadius: number = 0
|
| 97 |
+
public waveStrength: number = 0
|
| 98 |
+
public isWaveActive: boolean = false
|
| 99 |
+
|
| 100 |
+
public triggerWave(rangeEnergy: number): void {
|
| 101 |
+
const maxEnergy = 255
|
| 102 |
+
const energyExcess = rangeEnergy - 200
|
| 103 |
+
this.waveStrength = (energyExcess / (maxEnergy - 200)) * 20.0
|
| 104 |
+
this.currentWaveRadius = 0
|
| 105 |
+
this.isWaveActive = true
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
public update(deltaTime: number): void {
|
| 109 |
+
if (this.isWaveActive) {
|
| 110 |
+
this.currentWaveRadius += deltaTime * 1.0
|
| 111 |
+
this.waveStrength *= 0.98
|
| 112 |
+
|
| 113 |
+
if (this.currentWaveRadius > 1.0 || this.waveStrength < 0.1) {
|
| 114 |
+
this.isWaveActive = false
|
| 115 |
+
}
|
| 116 |
+
}
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
public getWaveForce(position: { x: number; y: number; z: number }): number {
|
| 120 |
+
if (!this.isWaveActive) return 0
|
| 121 |
+
|
| 122 |
+
const distanceFromCenter = Math.sqrt(position.x * position.x + position.y * position.y + position.z * position.z)
|
| 123 |
+
const distanceFromWave = Math.abs(distanceFromCenter - this.currentWaveRadius)
|
| 124 |
+
|
| 125 |
+
if (distanceFromWave < 0.1) {
|
| 126 |
+
return this.waveStrength * Math.exp(-distanceFromWave * 10)
|
| 127 |
+
}
|
| 128 |
+
return 0
|
| 129 |
+
}
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
// Utility functions for dynamic noise scaling
|
| 133 |
+
export function generateNewNoiseScale(
|
| 134 |
+
params: {
|
| 135 |
+
dynamicNoiseScale: boolean
|
| 136 |
+
minNoiseScale: number
|
| 137 |
+
maxNoiseScale: number
|
| 138 |
+
noiseStep: number
|
| 139 |
+
},
|
| 140 |
+
lastNoiseScale: number
|
| 141 |
+
): number {
|
| 142 |
+
if (!params.dynamicNoiseScale) {
|
| 143 |
+
return params.minNoiseScale
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
let { minNoiseScale, maxNoiseScale, noiseStep } = params
|
| 147 |
+
|
| 148 |
+
// Safety checks
|
| 149 |
+
if (minNoiseScale >= maxNoiseScale) {
|
| 150 |
+
console.warn(`Fixing minNoiseScale (${minNoiseScale}) >= maxNoiseScale (${maxNoiseScale}).`)
|
| 151 |
+
maxNoiseScale = minNoiseScale + 0.1
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
let range = maxNoiseScale - minNoiseScale
|
| 155 |
+
if (range < 0.1) {
|
| 156 |
+
console.warn(`Range < 0.1 => Forcing minimal range = 0.1`)
|
| 157 |
+
range = 0.1
|
| 158 |
+
maxNoiseScale = minNoiseScale + range
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
if (noiseStep > range) {
|
| 162 |
+
console.warn(`noiseStep (${noiseStep}) > range (${range}) => Forcing noiseStep = range / 2`)
|
| 163 |
+
noiseStep = range / 2
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
// Clamp lastNoiseScale to valid range
|
| 167 |
+
lastNoiseScale = Math.max(minNoiseScale, Math.min(lastNoiseScale, maxNoiseScale))
|
| 168 |
+
|
| 169 |
+
const stepsUp = Math.floor((maxNoiseScale - lastNoiseScale) / noiseStep)
|
| 170 |
+
const stepsDown = Math.floor((lastNoiseScale - minNoiseScale) / noiseStep)
|
| 171 |
+
|
| 172 |
+
if (stepsUp === 0 && stepsDown === 0) {
|
| 173 |
+
return lastNoiseScale
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
const direction = Math.random() < 0.5 && stepsDown > 0 ? -1 : 1
|
| 177 |
+
const steps = direction === 1
|
| 178 |
+
? Math.floor(Math.random() * (stepsUp + 1))
|
| 179 |
+
: Math.floor(Math.random() * (stepsDown + 1))
|
| 180 |
+
|
| 181 |
+
let newValue = lastNoiseScale + direction * steps * noiseStep
|
| 182 |
+
newValue = Math.max(minNoiseScale, Math.min(newValue, maxNoiseScale))
|
| 183 |
+
|
| 184 |
+
return newValue
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
// Global noise instance
|
| 188 |
+
export const noise = new NoiseGenerator()
|
src/utils/performanceOptimizer.ts
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Performance Optimization System for Web Audio Visualizer
|
| 2 |
+
// Optimized for Hugging Face Spaces free tier deployment
|
| 3 |
+
|
| 4 |
+
interface PerformanceMetrics {
|
| 5 |
+
fps: number
|
| 6 |
+
frameTime: number
|
| 7 |
+
memoryUsage: number
|
| 8 |
+
cpuUsage: number
|
| 9 |
+
gpuMemory: number
|
| 10 |
+
particleCount: number
|
| 11 |
+
lastUpdate: number
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
interface OptimizationSettings {
|
| 15 |
+
targetFPS: number
|
| 16 |
+
minFPS: number
|
| 17 |
+
maxParticles: number
|
| 18 |
+
minParticles: number
|
| 19 |
+
adaptiveQualityStep: number
|
| 20 |
+
memoryThreshold: number
|
| 21 |
+
cpuThreshold: number
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
interface CacheEntry {
|
| 25 |
+
data: any
|
| 26 |
+
timestamp: number
|
| 27 |
+
accessCount: number
|
| 28 |
+
size: number
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
export class PerformanceOptimizer {
|
| 32 |
+
private metrics: PerformanceMetrics
|
| 33 |
+
private settings: OptimizationSettings
|
| 34 |
+
private frameHistory: number[] = []
|
| 35 |
+
private lastFrameTime: number = 0
|
| 36 |
+
private adaptiveLevel: number = 1.0 // 1.0 = max quality, 0.1 = min quality
|
| 37 |
+
|
| 38 |
+
// Caching system
|
| 39 |
+
private shaderCache: Map<string, any> = new Map()
|
| 40 |
+
private geometryCache: Map<string, any> = new Map()
|
| 41 |
+
private materialCache: Map<string, any> = new Map()
|
| 42 |
+
private calculationCache: Map<string, CacheEntry> = new Map()
|
| 43 |
+
|
| 44 |
+
// Object pools for reuse
|
| 45 |
+
private bufferPool: Map<string, Float32Array[]> = new Map()
|
| 46 |
+
|
| 47 |
+
// Performance monitoring
|
| 48 |
+
private performanceObserver: PerformanceObserver | null = null
|
| 49 |
+
private memoryInfo: any = null
|
| 50 |
+
|
| 51 |
+
constructor(settings: Partial<OptimizationSettings> = {}) {
|
| 52 |
+
this.settings = {
|
| 53 |
+
targetFPS: 60,
|
| 54 |
+
minFPS: 30,
|
| 55 |
+
maxParticles: 15000,
|
| 56 |
+
minParticles: 3000,
|
| 57 |
+
adaptiveQualityStep: 0.05,
|
| 58 |
+
memoryThreshold: 100 * 1024 * 1024, // 100MB
|
| 59 |
+
cpuThreshold: 80, // 80% CPU usage
|
| 60 |
+
...settings
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
this.metrics = {
|
| 64 |
+
fps: 60,
|
| 65 |
+
frameTime: 16.67,
|
| 66 |
+
memoryUsage: 0,
|
| 67 |
+
cpuUsage: 0,
|
| 68 |
+
gpuMemory: 0,
|
| 69 |
+
particleCount: this.settings.maxParticles,
|
| 70 |
+
lastUpdate: performance.now()
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
this.initializePerformanceMonitoring()
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
private initializePerformanceMonitoring() {
|
| 77 |
+
// Modern performance monitoring
|
| 78 |
+
if ('PerformanceObserver' in window) {
|
| 79 |
+
this.performanceObserver = new PerformanceObserver((_list) => {
|
| 80 |
+
// Performance entries are processed in updatePerformance method
|
| 81 |
+
})
|
| 82 |
+
try {
|
| 83 |
+
this.performanceObserver.observe({ entryTypes: ['measure'] })
|
| 84 |
+
} catch (e) {
|
| 85 |
+
console.warn('Performance monitoring not fully supported')
|
| 86 |
+
}
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
// Memory monitoring for supported browsers
|
| 90 |
+
if ('memory' in performance) {
|
| 91 |
+
this.memoryInfo = (performance as any).memory
|
| 92 |
+
}
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
// Adaptive quality management
|
| 96 |
+
updatePerformance(currentTime: number): void {
|
| 97 |
+
const deltaTime = currentTime - this.lastFrameTime
|
| 98 |
+
this.lastFrameTime = currentTime
|
| 99 |
+
|
| 100 |
+
if (deltaTime > 0) {
|
| 101 |
+
const currentFPS = 1000 / deltaTime
|
| 102 |
+
this.frameHistory.push(currentFPS)
|
| 103 |
+
|
| 104 |
+
// Keep only last 60 frames for rolling average
|
| 105 |
+
if (this.frameHistory.length > 60) {
|
| 106 |
+
this.frameHistory.shift()
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
// Calculate average FPS
|
| 110 |
+
const avgFPS = this.frameHistory.reduce((a, b) => a + b, 0) / this.frameHistory.length
|
| 111 |
+
this.metrics.fps = Math.round(avgFPS)
|
| 112 |
+
this.metrics.frameTime = deltaTime
|
| 113 |
+
|
| 114 |
+
// Update memory usage if available
|
| 115 |
+
if (this.memoryInfo) {
|
| 116 |
+
this.metrics.memoryUsage = this.memoryInfo.usedJSHeapSize
|
| 117 |
+
this.metrics.gpuMemory = this.memoryInfo.totalJSHeapSize
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
// Adaptive quality adjustment
|
| 121 |
+
this.adjustQualityLevel()
|
| 122 |
+
}
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
private adjustQualityLevel(): void {
|
| 126 |
+
const { fps } = this.metrics
|
| 127 |
+
const { targetFPS, minFPS, adaptiveQualityStep } = this.settings
|
| 128 |
+
|
| 129 |
+
if (fps < minFPS) {
|
| 130 |
+
// Performance is bad, reduce quality aggressively
|
| 131 |
+
this.adaptiveLevel = Math.max(0.1, this.adaptiveLevel - adaptiveQualityStep * 2)
|
| 132 |
+
console.warn(`🚨 Performance low (${fps} FPS), reducing quality to ${(this.adaptiveLevel * 100).toFixed(0)}%`)
|
| 133 |
+
} else if (fps < targetFPS) {
|
| 134 |
+
// Performance is below target, reduce quality gradually
|
| 135 |
+
this.adaptiveLevel = Math.max(0.1, this.adaptiveLevel - adaptiveQualityStep)
|
| 136 |
+
} else if (fps > targetFPS + 10 && this.adaptiveLevel < 1.0) {
|
| 137 |
+
// Performance is good, increase quality gradually
|
| 138 |
+
this.adaptiveLevel = Math.min(1.0, this.adaptiveLevel + adaptiveQualityStep * 0.5)
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
// Update particle count based on quality level
|
| 142 |
+
const targetParticles = Math.floor(
|
| 143 |
+
this.settings.minParticles +
|
| 144 |
+
(this.settings.maxParticles - this.settings.minParticles) * this.adaptiveLevel
|
| 145 |
+
)
|
| 146 |
+
this.metrics.particleCount = targetParticles
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
// Smart caching system
|
| 150 |
+
getCachedCalculation(key: string, calculator: () => any, ttl: number = 1000): any {
|
| 151 |
+
const cached = this.calculationCache.get(key)
|
| 152 |
+
const now = performance.now()
|
| 153 |
+
|
| 154 |
+
if (cached && (now - cached.timestamp) < ttl) {
|
| 155 |
+
cached.accessCount++
|
| 156 |
+
return cached.data
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
// Calculate new value
|
| 160 |
+
const data = calculator()
|
| 161 |
+
this.calculationCache.set(key, {
|
| 162 |
+
data,
|
| 163 |
+
timestamp: now,
|
| 164 |
+
accessCount: 1,
|
| 165 |
+
size: this.estimateSize(data)
|
| 166 |
+
})
|
| 167 |
+
|
| 168 |
+
// Clean cache if it gets too large
|
| 169 |
+
this.cleanCache()
|
| 170 |
+
return data
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
// Shader caching for Three.js materials
|
| 174 |
+
getCachedShader(vertexShader: string, fragmentShader: string, uniforms: any): any {
|
| 175 |
+
const key = this.hashShader(vertexShader, fragmentShader)
|
| 176 |
+
|
| 177 |
+
if (this.shaderCache.has(key)) {
|
| 178 |
+
const cached = this.shaderCache.get(key)
|
| 179 |
+
// Update uniforms on cached shader
|
| 180 |
+
Object.assign(cached.uniforms, uniforms)
|
| 181 |
+
return cached
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
// Create new shader (this would be done in the component)
|
| 185 |
+
return null // Component will create and cache
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
cacheShader(vertexShader: string, fragmentShader: string, material: any): void {
|
| 189 |
+
const key = this.hashShader(vertexShader, fragmentShader)
|
| 190 |
+
this.shaderCache.set(key, material)
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
// Geometry and buffer pooling
|
| 194 |
+
getPooledBuffer(type: string, size: number): Float32Array {
|
| 195 |
+
const poolKey = `${type}_${size}`
|
| 196 |
+
|
| 197 |
+
if (!this.bufferPool.has(poolKey)) {
|
| 198 |
+
this.bufferPool.set(poolKey, [])
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
const pool = this.bufferPool.get(poolKey)!
|
| 202 |
+
|
| 203 |
+
if (pool.length > 0) {
|
| 204 |
+
const buffer = pool.pop()!
|
| 205 |
+
buffer.fill(0) // Reset buffer
|
| 206 |
+
return buffer
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
// Create new buffer if pool is empty
|
| 210 |
+
return new Float32Array(size)
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
returnPooledBuffer(type: string, size: number, buffer: Float32Array): void {
|
| 214 |
+
const poolKey = `${type}_${size}`
|
| 215 |
+
|
| 216 |
+
if (!this.bufferPool.has(poolKey)) {
|
| 217 |
+
this.bufferPool.set(poolKey, [])
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
const pool = this.bufferPool.get(poolKey)!
|
| 221 |
+
|
| 222 |
+
// Limit pool size to prevent memory bloat
|
| 223 |
+
if (pool.length < 5) {
|
| 224 |
+
pool.push(buffer)
|
| 225 |
+
}
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
// Level of Detail (LOD) calculations
|
| 229 |
+
getLODSettings(distance: number, importance: number = 1.0): {
|
| 230 |
+
particleSize: number
|
| 231 |
+
updateFrequency: number
|
| 232 |
+
detailLevel: number
|
| 233 |
+
} {
|
| 234 |
+
const distanceFactor = Math.max(0.1, Math.min(1.0, 5.0 / distance))
|
| 235 |
+
const qualityFactor = this.adaptiveLevel * importance
|
| 236 |
+
|
| 237 |
+
return {
|
| 238 |
+
particleSize: distanceFactor * qualityFactor,
|
| 239 |
+
updateFrequency: Math.max(1, Math.floor(10 / (distanceFactor * qualityFactor))),
|
| 240 |
+
detailLevel: distanceFactor * qualityFactor
|
| 241 |
+
}
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
// Frame skipping for heavy operations
|
| 245 |
+
shouldSkipFrame(_operation: string, frequency: number): boolean {
|
| 246 |
+
const frameCount = Math.floor(performance.now() / 16.67) // Approximate frame count
|
| 247 |
+
const skipPattern = Math.max(1, Math.floor(frequency / this.adaptiveLevel))
|
| 248 |
+
|
| 249 |
+
return frameCount % skipPattern !== 0
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
// Memory management
|
| 253 |
+
private cleanCache(): void {
|
| 254 |
+
const now = performance.now()
|
| 255 |
+
const maxCacheAge = 30000 // 30 seconds
|
| 256 |
+
const maxCacheSize = 50 * 1024 * 1024 // 50MB
|
| 257 |
+
|
| 258 |
+
let totalSize = 0
|
| 259 |
+
const entries = Array.from(this.calculationCache.entries())
|
| 260 |
+
|
| 261 |
+
// Calculate total cache size
|
| 262 |
+
entries.forEach(([_, entry]) => {
|
| 263 |
+
totalSize += entry.size
|
| 264 |
+
})
|
| 265 |
+
|
| 266 |
+
// Clean old or least used entries if cache is too large
|
| 267 |
+
if (totalSize > maxCacheSize) {
|
| 268 |
+
entries
|
| 269 |
+
.sort((a, b) => {
|
| 270 |
+
const scoreA = a[1].accessCount / (now - a[1].timestamp)
|
| 271 |
+
const scoreB = b[1].accessCount / (now - b[1].timestamp)
|
| 272 |
+
return scoreA - scoreB // Lower score = less valuable
|
| 273 |
+
})
|
| 274 |
+
.slice(0, Math.floor(entries.length * 0.3)) // Remove 30% least valuable
|
| 275 |
+
.forEach(([key, _]) => {
|
| 276 |
+
this.calculationCache.delete(key)
|
| 277 |
+
})
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
// Remove old entries
|
| 281 |
+
entries.forEach(([key, entry]) => {
|
| 282 |
+
if (now - entry.timestamp > maxCacheAge) {
|
| 283 |
+
this.calculationCache.delete(key)
|
| 284 |
+
}
|
| 285 |
+
})
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
// Utility functions
|
| 289 |
+
private hashShader(vertex: string, fragment: string): string {
|
| 290 |
+
// Simple hash function for shader caching
|
| 291 |
+
let hash = 0
|
| 292 |
+
const str = vertex + fragment
|
| 293 |
+
for (let i = 0; i < str.length; i++) {
|
| 294 |
+
const char = str.charCodeAt(i)
|
| 295 |
+
hash = ((hash << 5) - hash) + char
|
| 296 |
+
hash = hash & hash // Convert to 32-bit integer
|
| 297 |
+
}
|
| 298 |
+
return hash.toString()
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
private estimateSize(obj: any): number {
|
| 302 |
+
// Rough size estimation for caching
|
| 303 |
+
const str = JSON.stringify(obj)
|
| 304 |
+
return str.length * 2 // Approximate bytes (UTF-16)
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
// Public getters
|
| 308 |
+
getMetrics(): PerformanceMetrics {
|
| 309 |
+
return { ...this.metrics }
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
getAdaptiveLevel(): number {
|
| 313 |
+
return this.adaptiveLevel
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
getOptimalParticleCount(baseCount: number): number {
|
| 317 |
+
return Math.floor(baseCount * this.adaptiveLevel)
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
getOptimalUpdateFrequency(baseFrequency: number): number {
|
| 321 |
+
return Math.max(1, Math.floor(baseFrequency / this.adaptiveLevel))
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
// Cleanup
|
| 325 |
+
dispose(): void {
|
| 326 |
+
if (this.performanceObserver) {
|
| 327 |
+
this.performanceObserver.disconnect()
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
this.shaderCache.clear()
|
| 331 |
+
this.geometryCache.clear()
|
| 332 |
+
this.materialCache.clear()
|
| 333 |
+
this.calculationCache.clear()
|
| 334 |
+
this.bufferPool.clear()
|
| 335 |
+
}
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
// Singleton instance for global use
|
| 339 |
+
export const performanceOptimizer = new PerformanceOptimizer({
|
| 340 |
+
targetFPS: 60,
|
| 341 |
+
minFPS: 25, // Lower threshold for Hugging Face Spaces
|
| 342 |
+
maxParticles: 8000, // Reduced for CPU deployment
|
| 343 |
+
minParticles: 2000,
|
| 344 |
+
adaptiveQualityStep: 0.08,
|
| 345 |
+
memoryThreshold: 80 * 1024 * 1024, // 80MB for free tier
|
| 346 |
+
cpuThreshold: 75
|
| 347 |
+
})
|
tsconfig.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"target": "ES2020",
|
| 4 |
+
"useDefineForClassFields": true,
|
| 5 |
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
| 6 |
+
"module": "ESNext",
|
| 7 |
+
"skipLibCheck": true,
|
| 8 |
+
|
| 9 |
+
/* Bundler mode */
|
| 10 |
+
"moduleResolution": "bundler",
|
| 11 |
+
"allowImportingTsExtensions": true,
|
| 12 |
+
"resolveJsonModule": true,
|
| 13 |
+
"isolatedModules": true,
|
| 14 |
+
"noEmit": true,
|
| 15 |
+
"jsx": "react-jsx",
|
| 16 |
+
|
| 17 |
+
/* Linting */
|
| 18 |
+
"strict": true,
|
| 19 |
+
"noUnusedLocals": true,
|
| 20 |
+
"noUnusedParameters": true,
|
| 21 |
+
"noFallthroughCasesInSwitch": true
|
| 22 |
+
},
|
| 23 |
+
"include": ["src"],
|
| 24 |
+
"references": [{ "path": "./tsconfig.node.json" }]
|
| 25 |
+
}
|
tsconfig.node.json
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"composite": true,
|
| 4 |
+
"skipLibCheck": true,
|
| 5 |
+
"module": "ESNext",
|
| 6 |
+
"moduleResolution": "bundler",
|
| 7 |
+
"allowSyntheticDefaultImports": true
|
| 8 |
+
},
|
| 9 |
+
"include": ["vite.config.ts"]
|
| 10 |
+
}
|
vite.config.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig } from 'vite'
|
| 2 |
+
import react from '@vitejs/plugin-react'
|
| 3 |
+
|
| 4 |
+
// https://vitejs.dev/config/
|
| 5 |
+
export default defineConfig({
|
| 6 |
+
plugins: [react()],
|
| 7 |
+
server: {
|
| 8 |
+
host: true,
|
| 9 |
+
port: 7860, // Hugging Face Spaces default port
|
| 10 |
+
},
|
| 11 |
+
build: {
|
| 12 |
+
// Optimization for Hugging Face Spaces deployment
|
| 13 |
+
target: 'es2015',
|
| 14 |
+
minify: 'terser',
|
| 15 |
+
sourcemap: false,
|
| 16 |
+
chunkSizeWarningLimit: 1000,
|
| 17 |
+
rollupOptions: {
|
| 18 |
+
output: {
|
| 19 |
+
manualChunks: {
|
| 20 |
+
// Separate Three.js into its own chunk for better caching
|
| 21 |
+
'three': ['three'],
|
| 22 |
+
'three-addons': ['three/examples/jsm/controls/OrbitControls.js'],
|
| 23 |
+
// Separate React into its own chunk
|
| 24 |
+
'react-vendor': ['react', 'react-dom'],
|
| 25 |
+
// Audio processing utilities
|
| 26 |
+
'audio-engine': ['./src/engine/AudioEngine.ts'],
|
| 27 |
+
// Performance optimization utilities
|
| 28 |
+
'performance': ['./src/utils/performanceOptimizer.ts']
|
| 29 |
+
},
|
| 30 |
+
// Optimize chunk naming for caching
|
| 31 |
+
chunkFileNames: (chunkInfo) => {
|
| 32 |
+
const facadeModuleId = chunkInfo.facadeModuleId ? chunkInfo.facadeModuleId.split('/').pop()?.replace('.ts', '').replace('.tsx', '') : 'chunk'
|
| 33 |
+
return `assets/${facadeModuleId}-[hash].js`
|
| 34 |
+
}
|
| 35 |
+
}
|
| 36 |
+
},
|
| 37 |
+
terserOptions: {
|
| 38 |
+
compress: {
|
| 39 |
+
drop_console: true, // Remove console logs in production
|
| 40 |
+
drop_debugger: true,
|
| 41 |
+
pure_funcs: ['console.log', 'console.warn'], // Remove specific console functions
|
| 42 |
+
},
|
| 43 |
+
mangle: {
|
| 44 |
+
safari10: true, // Ensure Safari compatibility
|
| 45 |
+
}
|
| 46 |
+
}
|
| 47 |
+
},
|
| 48 |
+
optimizeDeps: {
|
| 49 |
+
// Pre-bundle dependencies for faster loading
|
| 50 |
+
include: [
|
| 51 |
+
'react',
|
| 52 |
+
'react-dom',
|
| 53 |
+
'three',
|
| 54 |
+
'three/examples/jsm/controls/OrbitControls.js'
|
| 55 |
+
],
|
| 56 |
+
exclude: [
|
| 57 |
+
// Exclude large optional dependencies that might not be needed
|
| 58 |
+
]
|
| 59 |
+
},
|
| 60 |
+
// Asset optimization
|
| 61 |
+
assetsInclude: ['**/*.mp3', '**/*.wav', '**/*.ogg'],
|
| 62 |
+
|
| 63 |
+
// Environment variables for optimization
|
| 64 |
+
define: {
|
| 65 |
+
'__PERFORMANCE_MODE__': true // Enable performance mode by default
|
| 66 |
+
}
|
| 67 |
+
})
|