Severian commited on
Commit
6a03d7f
·
verified ·
1 Parent(s): e990583

Upload 32 files

Browse files
.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
+ })