@neo-ui-kit/react 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +32 -4
  2. package/dist/components/artwork/DatabaseArt/styles.css +58 -58
  3. package/dist/components/artwork/Landscape/styles.css +79 -79
  4. package/dist/components/artwork/Planet/styles.css +52 -52
  5. package/dist/components/artwork/QuantumField/QuantumFieldExperience.d.ts +6 -2
  6. package/dist/components/artwork/QuantumField/UPSTREAM-LICENSE.txt +21 -21
  7. package/dist/components/artwork/ServerArt/styles.css +43 -43
  8. package/dist/components/artwork/Spectrum/styles.css +35 -35
  9. package/dist/components/controls/Autocomplete/styles.css +1 -1
  10. package/dist/components/controls/Checkbox/styles.css +58 -58
  11. package/dist/components/controls/InputBase/styles.css +374 -374
  12. package/dist/components/controls/Link/styles.css +50 -50
  13. package/dist/components/controls/SegmentedControl/styles.css +9 -9
  14. package/dist/components/controls/Switch/styles.css +54 -54
  15. package/dist/components/controls/Tabs/styles.css +239 -239
  16. package/dist/components/controls/ToggleButton/styles.css +49 -49
  17. package/dist/components/controls/shared.css +694 -694
  18. package/dist/components/editor/shared.css +1 -1
  19. package/dist/components/effects/Beacon/styles.css +38 -38
  20. package/dist/components/effects/Equalizer/styles.css +34 -34
  21. package/dist/components/effects/GlowText/styles.css +56 -56
  22. package/dist/components/effects/Marquee/styles.css +46 -46
  23. package/dist/components/effects/Reveal/styles.css +25 -25
  24. package/dist/components/effects/Shimmer/styles.css +48 -48
  25. package/dist/components/effects/Sparkles/styles.css +28 -28
  26. package/dist/components/effects/Spotlight/styles.css +45 -45
  27. package/dist/components/effects/Tilt/styles.css +67 -67
  28. package/dist/components/feedback/CollapsibleSection/styles.css +76 -76
  29. package/dist/components/feedback/DataTable/styles.css +522 -522
  30. package/dist/components/feedback/EmptyState/styles.css +80 -80
  31. package/dist/components/feedback/KeyValueList/styles.css +51 -51
  32. package/dist/components/feedback/MenuItem/styles.css +71 -71
  33. package/dist/components/feedback/Popover/styles.css +44 -44
  34. package/dist/components/feedback/Steps/styles.css +152 -152
  35. package/dist/components/feedback/Toast/styles.css +89 -89
  36. package/dist/components/forms/FormFields/styles.css +21 -21
  37. package/dist/components/foundation/ThemeProvider/styles.css +31 -31
  38. package/dist/components/layout/Avatar/styles.css +151 -151
  39. package/dist/components/layout/Card/styles.css +37 -37
  40. package/dist/components/layout/DialogActions/styles.css +31 -31
  41. package/dist/components/layout/DialogBody/styles.css +17 -17
  42. package/dist/components/layout/Illustration/styles.css +104 -104
  43. package/dist/components/layout/ScrollArea/styles.css +61 -61
  44. package/dist/components/layout/TabPanel/styles.css +16 -16
  45. package/dist/components/layout/shared.css +65 -65
  46. package/dist/components/media/AudioPlayer/styles.css +86 -86
  47. package/dist/components/media/LevelMeter/styles.css +30 -30
  48. package/dist/components/media/MelodyRoll/styles.css +196 -196
  49. package/dist/components/media/RotaryKnob/styles.css +319 -319
  50. package/dist/components/media/Sparkline/styles.css +69 -69
  51. package/dist/components/media/WaveDecoration/styles.css +1 -1
  52. package/dist/components/media/shared.css +7 -7
  53. package/dist/fonts/melodix.css +123 -123
  54. package/dist/index.d.ts +2 -2
  55. package/dist/index.js +2106 -2104
  56. package/dist/theme/base.css +210 -210
  57. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -5514,366 +5514,366 @@ function PlaybackTrack({ audio, playing, position: controlledPosition, disabled,
5514
5514
  // src/components/artwork/QuantumField/reference.html?raw
5515
5515
  var reference_default = `<!DOCTYPE html>
5516
5516
  <!-- Quantum Fields Contributors, MIT License. Full notice: UPSTREAM-LICENSE.txt in this package. -->
5517
- <html lang="en">\r
5518
- \r
5519
- <head>\r
5520
- <meta charset="UTF-8">\r
5521
- <title>Quantum Field: Interactive</title>\r
5522
- <style>\r
5523
- @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=Space+Grotesk:wght@400;500;600&display=swap');\r
5524
- \r
5525
- * {\r
5526
- box-sizing: border-box;\r
5527
- }\r
5528
- \r
5529
- :root {\r
5530
- --ar: 167; --ag: 139; --ab: 250;\r
5531
- }\r
5532
- \r
5517
+ <html lang="en">
5518
+
5519
+ <head>
5520
+ <meta charset="UTF-8">
5521
+ <title>Quantum Field: Interactive</title>
5522
+ <style>
5523
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=Space+Grotesk:wght@400;500;600&display=swap');
5524
+
5525
+ * {
5526
+ box-sizing: border-box;
5527
+ }
5528
+
5529
+ :root {
5530
+ --ar: 167; --ag: 139; --ab: 250;
5531
+ }
5532
+
5533
5533
  html, body {
5534
5534
  margin: 0;
5535
5535
  overflow: hidden;
5536
5536
  background: transparent !important;
5537
5537
  color-scheme: dark;
5538
5538
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
5539
- }\r
5540
- \r
5541
- canvas {\r
5542
- display: block;\r
5543
- }\r
5544
- \r
5545
- #scanlines {\r
5546
- position: absolute;\r
5547
- top: 0;\r
5548
- left: 0;\r
5549
- width: 100%;\r
5550
- height: 100%;\r
5551
- background: radial-gradient(ellipse at center, transparent 0%, transparent 50%, rgba(0, 0, 0, 0.5) 100%);\r
5552
- pointer-events: none;\r
5553
- z-index: 10;\r
5554
- }\r
5555
- \r
5556
- #overlay {\r
5557
- position: absolute;\r
5558
- top: 0;\r
5559
- left: 0;\r
5560
- width: 100%;\r
5561
- height: 100%;\r
5562
- display: flex;\r
5563
- flex-direction: column;\r
5564
- justify-content: center;\r
5565
- align-items: center;\r
5566
- background: linear-gradient(180deg, rgba(15, 10, 25, 0.97) 0%, rgba(10, 10, 20, 0.98) 100%);\r
5567
- z-index: 100;\r
5568
- backdrop-filter: blur(20px);\r
5569
- transition: opacity 1s ease;\r
5570
- pointer-events: none;\r
5571
- /* Allow clicks to pass through to canvas */\r
5572
- }\r
5573
- \r
5574
- .hud-panel {\r
5575
- background: linear-gradient(165deg, rgba(255, 255, 255, 0.04) 0%, rgba(255, 255, 255, 0.01) 100%);\r
5576
- border: 1px solid rgba(255, 255, 255, 0.07);\r
5577
- padding: 55px 75px;\r
5578
- text-align: center;\r
5579
- border-radius: 28px;\r
5580
- box-shadow: 0 30px 80px rgba(0, 0, 0, 0.5);\r
5581
- pointer-events: auto;\r
5582
- /* Re-enable clicks for buttons */\r
5583
- }\r
5584
- \r
5585
- h1 {\r
5586
- font-family: 'Space Grotesk', sans-serif;\r
5587
- font-weight: 500;\r
5588
- letter-spacing: 1px;\r
5589
- font-size: 34px;\r
5590
- margin: 0 0 8px 0;\r
5591
- background: linear-gradient(135deg, #ffffff 0%, rgba(var(--ar),var(--ag),var(--ab),0.75) 40%, rgb(var(--ar),var(--ag),var(--ab)) 100%);\r
5592
- -webkit-background-clip: text;\r
5593
- -webkit-text-fill-color: transparent;\r
5594
- background-clip: text;\r
5595
- }\r
5596
- \r
5597
- .hud-panel p {\r
5598
- color: rgba(255, 255, 255, 0.35);\r
5599
- font-size: 13px;\r
5600
- margin-bottom: 40px;\r
5601
- letter-spacing: 0.5px;\r
5602
- font-weight: 300;\r
5603
- }\r
5604
- \r
5605
- .btn-group {\r
5606
- display: flex;\r
5607
- gap: 14px;\r
5608
- justify-content: center;\r
5609
- }\r
5610
- \r
5611
- button,\r
5612
- .file-upload {\r
5613
- background: linear-gradient(135deg, rgba(var(--ar),var(--ag),var(--ab),0.12) 0%, rgba(var(--ar),var(--ag),var(--ab),0.06) 100%);\r
5614
- border: 1px solid rgba(var(--ar),var(--ag),var(--ab),0.25);\r
5615
- color: rgba(255, 255, 255, 0.85);\r
5616
- padding: 15px 30px;\r
5617
- font-size: 13px;\r
5618
- font-weight: 500;\r
5619
- cursor: pointer;\r
5620
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\r
5621
- letter-spacing: 0.3px;\r
5622
- border-radius: 14px;\r
5623
- font-family: 'Inter', sans-serif;\r
5624
- }\r
5625
- \r
5626
- button:hover,\r
5627
- .file-upload:hover {\r
5628
- background: linear-gradient(135deg, rgba(var(--ar),var(--ag),var(--ab),0.25) 0%, rgba(var(--ar),var(--ag),var(--ab),0.18) 100%);\r
5629
- border-color: rgba(var(--ar),var(--ag),var(--ab),0.45);\r
5630
- transform: translateY(-2px);\r
5631
- box-shadow: 0 12px 35px rgba(139, 92, 246, 0.2);\r
5632
- }\r
5633
- \r
5634
- input[type="file"] {\r
5635
- display: none;\r
5636
- }\r
5637
- \r
5638
- #status-bar {\r
5639
- position: absolute;\r
5640
- bottom: 80px;\r
5641
- left: 28px;\r
5642
- color: rgba(255, 255, 255, 0.3);\r
5643
- font-size: 10px;\r
5644
- letter-spacing: 0.5px;\r
5645
- z-index: 200;\r
5646
- pointer-events: none;\r
5647
- }\r
5648
- \r
5649
- #status-bar span {\r
5650
- color: rgba(196, 181, 253, 0.7);\r
5651
- font-weight: 500;\r
5652
- }\r
5653
- \r
5654
- #interaction-hint {\r
5655
- position: absolute;\r
5656
- top: 28px;\r
5657
- width: 100%;\r
5658
- text-align: center;\r
5659
- color: rgba(255, 255, 255, 0.5);\r
5660
- font-size: 11px;\r
5661
- letter-spacing: 2px;\r
5662
- pointer-events: none;\r
5663
- z-index: 9;\r
5664
- text-transform: lowercase;\r
5665
- }\r
5666
- \r
5667
- #zoom-indicator {\r
5668
- position: absolute;\r
5669
- bottom: 80px;\r
5670
- right: 28px;\r
5671
- color: rgba(255, 255, 255, 0.3);\r
5672
- font-size: 10px;\r
5673
- z-index: 20;\r
5674
- pointer-events: none;\r
5675
- }\r
5676
- \r
5677
- #zoom-indicator span {\r
5678
- color: rgba(196, 181, 253, 0.7);\r
5679
- }\r
5680
- \r
5681
- #audio-controls {\r
5682
- position: fixed;\r
5683
- bottom: 24px;\r
5684
- left: 50%;\r
5685
- transform: translateX(-50%);\r
5686
- display: flex;\r
5687
- gap: 10px;\r
5688
- align-items: center;\r
5689
- padding: 12px 20px;\r
5690
- background: linear-gradient(180deg, rgba(35, 28, 50, 0.88) 0%, rgba(22, 20, 38, 0.92) 100%);\r
5691
- border: 1px solid rgba(255, 255, 255, 0.05);\r
5692
- border-radius: 18px;\r
5693
- z-index: 200;\r
5694
- opacity: 0;\r
5695
- pointer-events: none;\r
5696
- transition: all 0.4s ease;\r
5697
- backdrop-filter: blur(25px);\r
5698
- }\r
5699
- \r
5700
- #audio-controls.visible {\r
5701
- opacity: 1;\r
5702
- pointer-events: auto;\r
5703
- }\r
5704
- \r
5705
- #audio-controls button,\r
5706
- #audio-controls .file-upload {\r
5707
- padding: 9px 14px;\r
5708
- font-size: 11px;\r
5709
- background: rgba(255, 255, 255, 0.03);\r
5710
- border: 1px solid rgba(255, 255, 255, 0.07);\r
5711
- color: rgba(255, 255, 255, 0.65);\r
5712
- border-radius: 10px;\r
5713
- }\r
5714
- \r
5715
- #play-pause-btn {\r
5716
- min-width: 78px;\r
5717
- background: linear-gradient(135deg, rgba(var(--ar),var(--ag),var(--ab),0.18) 0%, rgba(var(--ar),var(--ag),var(--ab),0.10) 100%) !important;\r
5718
- border-color: rgba(var(--ar),var(--ag),var(--ab),0.28) !important;\r
5719
- color: #e9d5ff !important;\r
5720
- }\r
5721
- \r
5722
- #play-pause-btn.playing {\r
5723
- background: linear-gradient(135deg, rgba(251, 113, 133, 0.18) 0%, rgba(244, 114, 182, 0.12) 100%) !important;\r
5724
- border-color: rgba(251, 113, 133, 0.35) !important;\r
5725
- }\r
5726
- \r
5727
- #progress-container {\r
5728
- width: 85px;\r
5729
- height: 4px;\r
5730
- background: rgba(255, 255, 255, 0.06);\r
5731
- border-radius: 4px;\r
5732
- cursor: pointer;\r
5733
- }\r
5734
- \r
5735
- #progress-bar {\r
5736
- height: 100%;\r
5737
- background: linear-gradient(90deg, rgb(var(--ar),var(--ag),var(--ab)), rgba(var(--ar),var(--ag),var(--ab),0.6), rgba(var(--ar),var(--ag),var(--ab),0.3));\r
5738
- border-radius: 4px;\r
5739
- width: 0%;\r
5740
- }\r
5741
- \r
5742
- #time-display {\r
5743
- color: rgba(255, 255, 255, 0.35);\r
5744
- font-size: 10px;\r
5745
- min-width: 62px;\r
5746
- text-align: center;\r
5747
- }\r
5748
- \r
5749
- #track-name {\r
5750
- color: rgba(255, 255, 255, 0.3);\r
5751
- font-size: 10px;\r
5752
- max-width: 85px;\r
5753
- overflow: hidden;\r
5754
- text-overflow: ellipsis;\r
5755
- white-space: nowrap;\r
5756
- }\r
5757
- \r
5539
+ }
5540
+
5541
+ canvas {
5542
+ display: block;
5543
+ }
5544
+
5545
+ #scanlines {
5546
+ position: absolute;
5547
+ top: 0;
5548
+ left: 0;
5549
+ width: 100%;
5550
+ height: 100%;
5551
+ background: radial-gradient(ellipse at center, transparent 0%, transparent 50%, rgba(0, 0, 0, 0.5) 100%);
5552
+ pointer-events: none;
5553
+ z-index: 10;
5554
+ }
5555
+
5556
+ #overlay {
5557
+ position: absolute;
5558
+ top: 0;
5559
+ left: 0;
5560
+ width: 100%;
5561
+ height: 100%;
5562
+ display: flex;
5563
+ flex-direction: column;
5564
+ justify-content: center;
5565
+ align-items: center;
5566
+ background: linear-gradient(180deg, rgba(15, 10, 25, 0.97) 0%, rgba(10, 10, 20, 0.98) 100%);
5567
+ z-index: 100;
5568
+ backdrop-filter: blur(20px);
5569
+ transition: opacity 1s ease;
5570
+ pointer-events: none;
5571
+ /* Allow clicks to pass through to canvas */
5572
+ }
5573
+
5574
+ .hud-panel {
5575
+ background: linear-gradient(165deg, rgba(255, 255, 255, 0.04) 0%, rgba(255, 255, 255, 0.01) 100%);
5576
+ border: 1px solid rgba(255, 255, 255, 0.07);
5577
+ padding: 55px 75px;
5578
+ text-align: center;
5579
+ border-radius: 28px;
5580
+ box-shadow: 0 30px 80px rgba(0, 0, 0, 0.5);
5581
+ pointer-events: auto;
5582
+ /* Re-enable clicks for buttons */
5583
+ }
5584
+
5585
+ h1 {
5586
+ font-family: 'Space Grotesk', sans-serif;
5587
+ font-weight: 500;
5588
+ letter-spacing: 1px;
5589
+ font-size: 34px;
5590
+ margin: 0 0 8px 0;
5591
+ background: linear-gradient(135deg, #ffffff 0%, rgba(var(--ar),var(--ag),var(--ab),0.75) 40%, rgb(var(--ar),var(--ag),var(--ab)) 100%);
5592
+ -webkit-background-clip: text;
5593
+ -webkit-text-fill-color: transparent;
5594
+ background-clip: text;
5595
+ }
5596
+
5597
+ .hud-panel p {
5598
+ color: rgba(255, 255, 255, 0.35);
5599
+ font-size: 13px;
5600
+ margin-bottom: 40px;
5601
+ letter-spacing: 0.5px;
5602
+ font-weight: 300;
5603
+ }
5604
+
5605
+ .btn-group {
5606
+ display: flex;
5607
+ gap: 14px;
5608
+ justify-content: center;
5609
+ }
5610
+
5611
+ button,
5612
+ .file-upload {
5613
+ background: linear-gradient(135deg, rgba(var(--ar),var(--ag),var(--ab),0.12) 0%, rgba(var(--ar),var(--ag),var(--ab),0.06) 100%);
5614
+ border: 1px solid rgba(var(--ar),var(--ag),var(--ab),0.25);
5615
+ color: rgba(255, 255, 255, 0.85);
5616
+ padding: 15px 30px;
5617
+ font-size: 13px;
5618
+ font-weight: 500;
5619
+ cursor: pointer;
5620
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
5621
+ letter-spacing: 0.3px;
5622
+ border-radius: 14px;
5623
+ font-family: 'Inter', sans-serif;
5624
+ }
5625
+
5626
+ button:hover,
5627
+ .file-upload:hover {
5628
+ background: linear-gradient(135deg, rgba(var(--ar),var(--ag),var(--ab),0.25) 0%, rgba(var(--ar),var(--ag),var(--ab),0.18) 100%);
5629
+ border-color: rgba(var(--ar),var(--ag),var(--ab),0.45);
5630
+ transform: translateY(-2px);
5631
+ box-shadow: 0 12px 35px rgba(139, 92, 246, 0.2);
5632
+ }
5633
+
5634
+ input[type="file"] {
5635
+ display: none;
5636
+ }
5637
+
5638
+ #status-bar {
5639
+ position: absolute;
5640
+ bottom: 80px;
5641
+ left: 28px;
5642
+ color: rgba(255, 255, 255, 0.3);
5643
+ font-size: 10px;
5644
+ letter-spacing: 0.5px;
5645
+ z-index: 200;
5646
+ pointer-events: none;
5647
+ }
5648
+
5649
+ #status-bar span {
5650
+ color: rgba(196, 181, 253, 0.7);
5651
+ font-weight: 500;
5652
+ }
5653
+
5654
+ #interaction-hint {
5655
+ position: absolute;
5656
+ top: 28px;
5657
+ width: 100%;
5658
+ text-align: center;
5659
+ color: rgba(255, 255, 255, 0.5);
5660
+ font-size: 11px;
5661
+ letter-spacing: 2px;
5662
+ pointer-events: none;
5663
+ z-index: 9;
5664
+ text-transform: lowercase;
5665
+ }
5666
+
5667
+ #zoom-indicator {
5668
+ position: absolute;
5669
+ bottom: 80px;
5670
+ right: 28px;
5671
+ color: rgba(255, 255, 255, 0.3);
5672
+ font-size: 10px;
5673
+ z-index: 20;
5674
+ pointer-events: none;
5675
+ }
5676
+
5677
+ #zoom-indicator span {
5678
+ color: rgba(196, 181, 253, 0.7);
5679
+ }
5680
+
5681
+ #audio-controls {
5682
+ position: fixed;
5683
+ bottom: 24px;
5684
+ left: 50%;
5685
+ transform: translateX(-50%);
5686
+ display: flex;
5687
+ gap: 10px;
5688
+ align-items: center;
5689
+ padding: 12px 20px;
5690
+ background: linear-gradient(180deg, rgba(35, 28, 50, 0.88) 0%, rgba(22, 20, 38, 0.92) 100%);
5691
+ border: 1px solid rgba(255, 255, 255, 0.05);
5692
+ border-radius: 18px;
5693
+ z-index: 200;
5694
+ opacity: 0;
5695
+ pointer-events: none;
5696
+ transition: all 0.4s ease;
5697
+ backdrop-filter: blur(25px);
5698
+ }
5699
+
5700
+ #audio-controls.visible {
5701
+ opacity: 1;
5702
+ pointer-events: auto;
5703
+ }
5704
+
5705
+ #audio-controls button,
5706
+ #audio-controls .file-upload {
5707
+ padding: 9px 14px;
5708
+ font-size: 11px;
5709
+ background: rgba(255, 255, 255, 0.03);
5710
+ border: 1px solid rgba(255, 255, 255, 0.07);
5711
+ color: rgba(255, 255, 255, 0.65);
5712
+ border-radius: 10px;
5713
+ }
5714
+
5715
+ #play-pause-btn {
5716
+ min-width: 78px;
5717
+ background: linear-gradient(135deg, rgba(var(--ar),var(--ag),var(--ab),0.18) 0%, rgba(var(--ar),var(--ag),var(--ab),0.10) 100%) !important;
5718
+ border-color: rgba(var(--ar),var(--ag),var(--ab),0.28) !important;
5719
+ color: #e9d5ff !important;
5720
+ }
5721
+
5722
+ #play-pause-btn.playing {
5723
+ background: linear-gradient(135deg, rgba(251, 113, 133, 0.18) 0%, rgba(244, 114, 182, 0.12) 100%) !important;
5724
+ border-color: rgba(251, 113, 133, 0.35) !important;
5725
+ }
5726
+
5727
+ #progress-container {
5728
+ width: 85px;
5729
+ height: 4px;
5730
+ background: rgba(255, 255, 255, 0.06);
5731
+ border-radius: 4px;
5732
+ cursor: pointer;
5733
+ }
5734
+
5735
+ #progress-bar {
5736
+ height: 100%;
5737
+ background: linear-gradient(90deg, rgb(var(--ar),var(--ag),var(--ab)), rgba(var(--ar),var(--ag),var(--ab),0.6), rgba(var(--ar),var(--ag),var(--ab),0.3));
5738
+ border-radius: 4px;
5739
+ width: 0%;
5740
+ }
5741
+
5742
+ #time-display {
5743
+ color: rgba(255, 255, 255, 0.35);
5744
+ font-size: 10px;
5745
+ min-width: 62px;
5746
+ text-align: center;
5747
+ }
5748
+
5749
+ #track-name {
5750
+ color: rgba(255, 255, 255, 0.3);
5751
+ font-size: 10px;
5752
+ max-width: 85px;
5753
+ overflow: hidden;
5754
+ text-overflow: ellipsis;
5755
+ white-space: nowrap;
5756
+ }
5757
+
5758
5758
  /* --- Center Panel Refinement --- */
5759
- .hud-panel {\r
5760
- background: rgba(15, 10, 25, 0.85);\r
5761
- border: 1px solid rgba(255, 255, 255, 0.08);\r
5762
- /* Darkened container */\r
5763
- padding: 24px;\r
5764
- /* Reduced padding */\r
5765
- border-radius: 24px;\r
5766
- box-shadow: 0 20px 50px rgba(0, 0, 0, 0.6);\r
5767
- backdrop-filter: blur(40px);\r
5768
- min-width: 420px;\r
5769
- /* Wider for buttons */\r
5770
- }\r
5771
- \r
5772
- .hud-panel h1,\r
5773
- .hud-panel p {\r
5774
- display: none;\r
5775
- /* Hide title as per screenshot showing only buttons */\r
5776
- }\r
5777
- \r
5778
- .btn-group {\r
5779
- display: flex;\r
5780
- gap: 16px;\r
5781
- justify-content: center;\r
5782
- width: 100%;\r
5783
- }\r
5784
- \r
5785
- .btn-group button {\r
5786
- flex: 1;\r
5787
- background: transparent;\r
5788
- border: 1px solid rgba(var(--ar),var(--ag),var(--ab),0.3);\r
5789
- color: rgb(var(--ar),var(--ag),var(--ab));\r
5790
- padding: 14px 0;\r
5791
- border-radius: 12px;\r
5792
- font-size: 12px;\r
5793
- letter-spacing: 1px;\r
5794
- text-transform: uppercase;\r
5795
- font-weight: 600;\r
5796
- transition: all 0.3s ease;\r
5797
- }\r
5798
- \r
5799
- .btn-group button:hover {\r
5800
- background: rgba(var(--ar),var(--ag),var(--ab),0.1);\r
5801
- border-color: rgb(var(--ar),var(--ag),var(--ab));\r
5802
- box-shadow: 0 0 20px rgba(var(--ar),var(--ag),var(--ab),0.2);\r
5803
- }\r
5759
+ .hud-panel {
5760
+ background: rgba(15, 10, 25, 0.85);
5761
+ border: 1px solid rgba(255, 255, 255, 0.08);
5762
+ /* Darkened container */
5763
+ padding: 24px;
5764
+ /* Reduced padding */
5765
+ border-radius: 24px;
5766
+ box-shadow: 0 20px 50px rgba(0, 0, 0, 0.6);
5767
+ backdrop-filter: blur(40px);
5768
+ min-width: 420px;
5769
+ /* Wider for buttons */
5770
+ }
5771
+
5772
+ .hud-panel h1,
5773
+ .hud-panel p {
5774
+ display: none;
5775
+ /* Hide title as per screenshot showing only buttons */
5776
+ }
5777
+
5778
+ .btn-group {
5779
+ display: flex;
5780
+ gap: 16px;
5781
+ justify-content: center;
5782
+ width: 100%;
5783
+ }
5784
+
5785
+ .btn-group button {
5786
+ flex: 1;
5787
+ background: transparent;
5788
+ border: 1px solid rgba(var(--ar),var(--ag),var(--ab),0.3);
5789
+ color: rgb(var(--ar),var(--ag),var(--ab));
5790
+ padding: 14px 0;
5791
+ border-radius: 12px;
5792
+ font-size: 12px;
5793
+ letter-spacing: 1px;
5794
+ text-transform: uppercase;
5795
+ font-weight: 600;
5796
+ transition: all 0.3s ease;
5797
+ }
5798
+
5799
+ .btn-group button:hover {
5800
+ background: rgba(var(--ar),var(--ag),var(--ab),0.1);
5801
+ border-color: rgb(var(--ar),var(--ag),var(--ab));
5802
+ box-shadow: 0 0 20px rgba(var(--ar),var(--ag),var(--ab),0.2);
5803
+ }
5804
5804
  #interaction-hint, #audio-controls, #status-bar, #zoom-indicator { display: none !important; }
5805
5805
  </style>
5806
- </head>\r
5807
- \r
5808
- <body>\r
5806
+ </head>
5807
+
5808
+ <body>
5809
5809
  <div id="scanlines" style="display:none"></div>
5810
- <div id="interaction-hint">space \xB7 play/pause &nbsp;\xB7&nbsp; scroll \xB7 zoom &nbsp;\xB7&nbsp; drag \xB7 rotate &nbsp;\xB7&nbsp; F \xB7 focus mode</div>\r
5810
+ <div id="interaction-hint">space \xB7 play/pause &nbsp;\xB7&nbsp; scroll \xB7 zoom &nbsp;\xB7&nbsp; drag \xB7 rotate &nbsp;\xB7&nbsp; F \xB7 focus mode</div>
5811
5811
  <div id="overlay" style="display:none">
5812
- <div class="hud-panel">\r
5813
- <h1>Quantum Fields</h1>\r
5814
- <p>Audio-reactive particle simulation</p>\r
5815
- <div class="btn-group">\r
5816
- <button id="enter-btn">Enter</button>\r
5817
- </div>\r
5818
- </div>\r
5819
- </div>\r
5820
- <div id="audio-controls">\r
5821
- <button id="play-pause-btn" class="playing">\u23F8 pause</button>\r
5822
- <div id="progress-container">\r
5823
- <div id="progress-bar"></div>\r
5824
- </div>\r
5825
- <span id="time-display">0:00 / 0:00</span>\r
5826
- <span id="track-name">no track</span>\r
5827
- <label class="file-upload" title="Load audio">\u{1F4C1}<input type="file" id="file-input-2" accept="audio/*"></label>\r
5828
- <button id="mic-btn-2" title="Microphone">\u{1F399}</button>\r
5829
- <button id="screenshot-btn" title="Screenshot">\u{1F4F7}</button>\r
5830
- <button id="fullscreen-btn" title="Fullscreen">\u26F6</button>\r
5831
- </div>\r
5832
- <div id="status-bar"><span>ready</span> \xB7 awaiting input</div>\r
5833
- <div id="zoom-indicator">zoom <span id="zoom-level">100%</span></div>\r
5834
- \r
5835
- <!-- Global Error Handler (Must be before module imports) -->\r
5836
- <script>\r
5837
- window.onerror = function (msg, url, lineNo, columnNo, error) {\r
5838
- const status = document.getElementById('status-bar');\r
5839
- if (status) {\r
5840
- status.innerHTML = \`ERROR: <span style="color:#ff6b6b">\${msg}</span>\`;\r
5841
- status.style.opacity = 1;\r
5842
- }\r
5843
- console.error('Global Error:', msg, url, lineNo, error);\r
5844
- return false;\r
5845
- };\r
5846
- // Catch unhandled promise rejections\r
5847
- window.onunhandledrejection = function (event) {\r
5848
- const status = document.getElementById('status-bar');\r
5849
- if (status) {\r
5850
- status.innerHTML = \`ERROR: <span style="color:#ff6b6b">\${event.reason}</span>\`;\r
5851
- status.style.opacity = 1;\r
5852
- }\r
5853
- console.error('Unhandled Rejection:', event.reason);\r
5854
- };\r
5855
- <\/script>\r
5856
- \r
5857
- <script type="importmap">\r
5858
- {\r
5859
- "imports": {\r
5860
- "three": "https://unpkg.com/three@0.182.0/build/three.module.js",\r
5812
+ <div class="hud-panel">
5813
+ <h1>Quantum Fields</h1>
5814
+ <p>Audio-reactive particle simulation</p>
5815
+ <div class="btn-group">
5816
+ <button id="enter-btn">Enter</button>
5817
+ </div>
5818
+ </div>
5819
+ </div>
5820
+ <div id="audio-controls">
5821
+ <button id="play-pause-btn" class="playing">\u23F8 pause</button>
5822
+ <div id="progress-container">
5823
+ <div id="progress-bar"></div>
5824
+ </div>
5825
+ <span id="time-display">0:00 / 0:00</span>
5826
+ <span id="track-name">no track</span>
5827
+ <label class="file-upload" title="Load audio">\u{1F4C1}<input type="file" id="file-input-2" accept="audio/*"></label>
5828
+ <button id="mic-btn-2" title="Microphone">\u{1F399}</button>
5829
+ <button id="screenshot-btn" title="Screenshot">\u{1F4F7}</button>
5830
+ <button id="fullscreen-btn" title="Fullscreen">\u26F6</button>
5831
+ </div>
5832
+ <div id="status-bar"><span>ready</span> \xB7 awaiting input</div>
5833
+ <div id="zoom-indicator">zoom <span id="zoom-level">100%</span></div>
5834
+
5835
+ <!-- Global Error Handler (Must be before module imports) -->
5836
+ <script>
5837
+ window.onerror = function (msg, url, lineNo, columnNo, error) {
5838
+ const status = document.getElementById('status-bar');
5839
+ if (status) {
5840
+ status.innerHTML = \`ERROR: <span style="color:#ff6b6b">\${msg}</span>\`;
5841
+ status.style.opacity = 1;
5842
+ }
5843
+ console.error('Global Error:', msg, url, lineNo, error);
5844
+ return false;
5845
+ };
5846
+ // Catch unhandled promise rejections
5847
+ window.onunhandledrejection = function (event) {
5848
+ const status = document.getElementById('status-bar');
5849
+ if (status) {
5850
+ status.innerHTML = \`ERROR: <span style="color:#ff6b6b">\${event.reason}</span>\`;
5851
+ status.style.opacity = 1;
5852
+ }
5853
+ console.error('Unhandled Rejection:', event.reason);
5854
+ };
5855
+ <\/script>
5856
+
5857
+ <script type="importmap">
5858
+ {
5859
+ "imports": {
5860
+ "three": "https://unpkg.com/three@0.182.0/build/three.module.js",
5861
5861
  "three/addons/": "https://unpkg.com/three@0.182.0/examples/jsm/"
5862
- }\r
5863
- }\r
5864
- <\/script>\r
5865
- \r
5866
- <script type="module">\r
5867
- import * as THREE from 'three';\r
5868
- import { OrbitControls } from 'three/addons/controls/OrbitControls.js';\r
5869
- import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';\r
5870
- import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';\r
5871
- import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';\r
5872
- import { AfterimagePass } from 'three/addons/postprocessing/AfterimagePass.js';\r
5873
- import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';\r
5874
- // BokehPass removed - using custom DOF shader\r
5875
- import { OutputPass } from 'three/addons/postprocessing/OutputPass.js';\r
5876
- \r
5862
+ }
5863
+ }
5864
+ <\/script>
5865
+
5866
+ <script type="module">
5867
+ import * as THREE from 'three';
5868
+ import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
5869
+ import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
5870
+ import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
5871
+ import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
5872
+ import { AfterimagePass } from 'three/addons/postprocessing/AfterimagePass.js';
5873
+ import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';
5874
+ // BokehPass removed - using custom DOF shader
5875
+ import { OutputPass } from 'three/addons/postprocessing/OutputPass.js';
5876
+
5877
5877
  function fitPixelRatio() {
5878
5878
  return Math.min(window.devicePixelRatio || 1, 1.5,
5879
5879
  Math.sqrt(2_100_000 / Math.max(1, window.innerWidth * window.innerHeight)));
@@ -5883,260 +5883,260 @@ var reference_default = `<!DOCTYPE html>
5883
5883
  maxParticles: 250000,
5884
5884
  maxDensity: 0.14,
5885
5885
  secondaryParticles: 50000,
5886
- initialDensity: 0.043,\r
5887
- fieldRadius: 90,\r
5888
- minZoom: 1,\r
5889
- maxZoom: 250,\r
5886
+ initialDensity: 0.043,
5887
+ fieldRadius: 90,
5888
+ minZoom: 1,
5889
+ maxZoom: 250,
5890
5890
  defaultZoom: 200
5891
5891
  };
5892
5892
  let particleBudget = Number(window.frameElement?.getAttribute('data-particle-budget') ?? Infinity);
5893
5893
  particleBudget = Number.isFinite(particleBudget) ? Math.max(0, Math.floor(particleBudget)) : Infinity;
5894
- \r
5895
- const STATE = {\r
5894
+
5895
+ const STATE = {
5896
5896
  field: 'Gluon (Strong)',
5897
- density: CONFIG.initialDensity,\r
5898
- sensitivity: 1.1,\r
5899
- trails: 0.66144,\r
5900
- bloom: 0.3,\r
5901
- timeScale: 1,\r
5897
+ density: CONFIG.initialDensity,
5898
+ sensitivity: 1.1,
5899
+ trails: 0.66144,
5900
+ bloom: 0.3,
5901
+ timeScale: 1,
5902
5902
  pixelRatio: fitPixelRatio(),
5903
- chromaticAberration: 0.004,\r
5904
- filmGrain: 0.001,\r
5905
- cameraShake: 0.65,\r
5906
- vortexStrength: 0.73,\r
5907
- pulseIntensity: 0.15,\r
5908
- anamorphicStretch: 0.1,\r
5909
- bassGateThreshold: 0.12,\r
5910
- bassGateAttack: 0.3,\r
5911
- bassGateRelease: 0.136,\r
5912
- bassGateRatio: 2.142,\r
5913
- bassGateEnabled: false,\r
5914
- // NEW EFFECTS\r
5915
- godRaysEnabled: false,\r
5916
- godRaysIntensity: 0.078,\r
5917
- godRaysDecay: 0.92,\r
5918
- dofEnabled: false,\r
5919
- dofFocus: 0.04,\r
5920
- dofFocalLength: 0.40,\r
5921
- dofBokehStrength: 0.36,\r
5922
- lensFlareEnabled: true,\r
5923
- lensFlareIntensity: 0.1,\r
5924
- particleTrailsEnabled: true,\r
5925
- particleTrailOpacity: 1,\r
5926
- nebulaEnabled: false,\r
5927
- nebulaIntensity: 0.3,\r
5928
- connectionsEnabled: true,\r
5929
- connectionThreshold: 20.0,\r
5930
- connectionOpacity: 0.75,\r
5931
- // === ENHANCEMENT PACK v2.0 ===\r
5932
- // 1. Motion Blur\r
5933
- motionBlurEnabled: true,\r
5934
- motionBlurStrength: 0.1,\r
5935
- // 2. Adaptive Quality\r
5903
+ chromaticAberration: 0.004,
5904
+ filmGrain: 0.001,
5905
+ cameraShake: 0.65,
5906
+ vortexStrength: 0.73,
5907
+ pulseIntensity: 0.15,
5908
+ anamorphicStretch: 0.1,
5909
+ bassGateThreshold: 0.12,
5910
+ bassGateAttack: 0.3,
5911
+ bassGateRelease: 0.136,
5912
+ bassGateRatio: 2.142,
5913
+ bassGateEnabled: false,
5914
+ // NEW EFFECTS
5915
+ godRaysEnabled: false,
5916
+ godRaysIntensity: 0.078,
5917
+ godRaysDecay: 0.92,
5918
+ dofEnabled: false,
5919
+ dofFocus: 0.04,
5920
+ dofFocalLength: 0.40,
5921
+ dofBokehStrength: 0.36,
5922
+ lensFlareEnabled: true,
5923
+ lensFlareIntensity: 0.1,
5924
+ particleTrailsEnabled: true,
5925
+ particleTrailOpacity: 1,
5926
+ nebulaEnabled: false,
5927
+ nebulaIntensity: 0.3,
5928
+ connectionsEnabled: true,
5929
+ connectionThreshold: 20.0,
5930
+ connectionOpacity: 0.75,
5931
+ // === ENHANCEMENT PACK v2.0 ===
5932
+ // 1. Motion Blur
5933
+ motionBlurEnabled: true,
5934
+ motionBlurStrength: 0.1,
5935
+ // 2. Adaptive Quality
5936
5936
  adaptiveQualityEnabled: true,
5937
5937
  targetFPS: 45,
5938
5938
  qualityLevel: 1,
5939
- // 3. Onset Detection\r
5940
- onsetSensitivity: 1,\r
5941
- onsetDecay: 0.92,\r
5942
- // 4. Audio-Reactive Camera\r
5943
- audioCameraEnabled: true,\r
5944
- audioCameraSmoothing: 0.2,\r
5945
- audioCameraIntensity: 0.3,\r
5946
- // 5. Waveform Terrain\r
5947
- waveformTerrainScale: 1.0,\r
5948
- waveformTerrainHeight: 20.0\r
5949
- };\r
5950
- \r
5951
- // --- AUDIO SYSTEM ---\r
5952
- const AUDIO = {\r
5953
- ctx: null, analyser: null, source: null, gainNode: null, data: null,\r
5954
- audioBuffer: null, isPlaying: false, isPaused: false,\r
5955
- startTime: 0, pauseTime: 0, currentTrackName: 'No Track',\r
5956
- mode: null, micStream: null,\r
5957
- bands: { subBass: 0, bass: 0, lowMid: 0, mid: 0, highMid: 0, high: 0, ultraHigh: 0 },\r
5958
- gatedBands: { subBass: 0, bass: 0, lowMid: 0, mid: 0, highMid: 0, high: 0, ultraHigh: 0 },\r
5959
- beatThreshold: 0.15, beatDecay: 0.93, beatEnergy: 0,\r
5960
- lastBeatTime: 0, beatDetected: false, peakHistory: [],\r
5939
+ // 3. Onset Detection
5940
+ onsetSensitivity: 1,
5941
+ onsetDecay: 0.92,
5942
+ // 4. Audio-Reactive Camera
5943
+ audioCameraEnabled: true,
5944
+ audioCameraSmoothing: 0.2,
5945
+ audioCameraIntensity: 0.3,
5946
+ // 5. Waveform Terrain
5947
+ waveformTerrainScale: 1.0,
5948
+ waveformTerrainHeight: 20.0
5949
+ };
5950
+
5951
+ // --- AUDIO SYSTEM ---
5952
+ const AUDIO = {
5953
+ ctx: null, analyser: null, source: null, gainNode: null, data: null,
5954
+ audioBuffer: null, isPlaying: false, isPaused: false,
5955
+ startTime: 0, pauseTime: 0, currentTrackName: 'No Track',
5956
+ mode: null, micStream: null,
5957
+ bands: { subBass: 0, bass: 0, lowMid: 0, mid: 0, highMid: 0, high: 0, ultraHigh: 0 },
5958
+ gatedBands: { subBass: 0, bass: 0, lowMid: 0, mid: 0, highMid: 0, high: 0, ultraHigh: 0 },
5959
+ beatThreshold: 0.15, beatDecay: 0.93, beatEnergy: 0,
5960
+ lastBeatTime: 0, beatDetected: false, peakHistory: [],
5961
5961
  spectralCentroid: 0, spectralFlux: 0, prevSpectrum: null, active: false, idleCleared: false,
5962
- // === ENHANCEMENT: Onset Detection ===\r
5963
- onsetEnergy: 0, onsetDetected: false, onsetHistory: [], onsetThreshold: 0,\r
5964
- // === ENHANCEMENT: Waveform Data ===\r
5965
- waveformData: new Float32Array(128), waveformSmoothed: new Float32Array(128),\r
5966
- gateState: {\r
5967
- isOpen: false, envelope: 0, holdTime: 0, noiseFloor: 0.05,\r
5968
- calibrationSamples: [], isCalibrating: true, calibrationFrames: 30\r
5969
- },\r
5970
- \r
5971
- init() {\r
5972
- if (this.ctx) return;\r
5973
- this.ctx = new (window.AudioContext || window.webkitAudioContext)();\r
5974
- this.analyser = this.ctx.createAnalyser();\r
5975
- this.analyser.fftSize = 4096;\r
5976
- this.analyser.smoothingTimeConstant = 0.65;\r
5977
- this.data = new Uint8Array(this.analyser.frequencyBinCount);\r
5978
- this.prevSpectrum = new Float32Array(this.analyser.frequencyBinCount);\r
5979
- this.active = true;\r
5980
- },\r
5981
- \r
5982
- loadTrack(arrayBuffer, fileName) {\r
5983
- return new Promise((resolve, reject) => {\r
5984
- this.ctx.decodeAudioData(arrayBuffer, (buffer) => {\r
5985
- this.stopCurrentSource();\r
5986
- this.audioBuffer = buffer;\r
5987
- this.currentTrackName = fileName || 'Unknown';\r
5988
- this.mode = 'file';\r
5989
- this.pauseTime = 0;\r
5990
- this.resetCalibration();\r
5991
- resolve(buffer);\r
5992
- }, reject);\r
5993
- });\r
5994
- },\r
5995
- \r
5996
- play() {\r
5997
- if (!this.audioBuffer || this.mode !== 'file' || this.isPlaying) return;\r
5998
- this.source = this.ctx.createBufferSource();\r
5999
- this.source.buffer = this.audioBuffer;\r
6000
- this.source.loop = true;\r
6001
- this.source.connect(this.analyser);\r
6002
- this.analyser.connect(this.ctx.destination);\r
6003
- this.source.start(0, this.pauseTime);\r
6004
- this.startTime = this.ctx.currentTime - this.pauseTime;\r
6005
- this.isPlaying = true;\r
6006
- this.isPaused = false;\r
6007
- },\r
6008
- \r
6009
- pause() {\r
6010
- if (!this.isPlaying || this.mode !== 'file') return;\r
6011
- this.pauseTime = this.getCurrentTime();\r
6012
- this.isPaused = true;\r
6013
- this.isPlaying = false;\r
6014
- if (this.source) { try { this.source.stop(); } catch (e) { } this.source.disconnect(); this.source = null; }\r
6015
- },\r
6016
- \r
6017
- togglePlayPause() {\r
6018
- if (this.mode === 'mic') return false;\r
6019
- if (this.isPlaying) { this.pause(); return false; }\r
6020
- else { this.play(); return true; }\r
6021
- },\r
6022
- \r
6023
- getCurrentTime() {\r
6024
- if (!this.isPlaying) return this.pauseTime;\r
6025
- if (!this.audioBuffer) return 0;\r
6026
- return (this.ctx.currentTime - this.startTime) % this.audioBuffer.duration;\r
6027
- },\r
6028
- \r
6029
- getDuration() { return this.audioBuffer ? this.audioBuffer.duration : 0; },\r
6030
- \r
6031
- seek(time) {\r
6032
- if (this.mode !== 'file' || !this.audioBuffer) return;\r
6033
- const wasPlaying = this.isPlaying;\r
6034
- if (wasPlaying) this.pause();\r
6035
- this.pauseTime = Math.max(0, Math.min(time, this.audioBuffer.duration));\r
6036
- if (wasPlaying) this.play();\r
6037
- },\r
6038
- \r
6039
- stopCurrentSource() {\r
6040
- if (this.source) { try { this.source.stop(); this.source.disconnect(); } catch (e) { } this.source = null; }\r
6041
- if (this.gainNode) { try { this.gainNode.disconnect(); } catch (e) { } this.gainNode = null; }\r
6042
- if (this.micStream) { this.micStream.getTracks().forEach(t => t.stop()); this.micStream = null; }\r
6043
- this.isPlaying = false; this.isPaused = false; this.pauseTime = 0;\r
6044
- },\r
6045
- \r
6046
- async startMic(preferredLabel = null) {\r
6047
- console.log('startMic called with preferredLabel:', preferredLabel);\r
6048
- const status = document.getElementById('status-bar');\r
6049
- if (status) status.innerHTML = 'SYSTEM: <span>REQUESTING ACCESS...</span>';\r
6050
- \r
6051
- try {\r
6052
- // Ensure context is running\r
6053
- if (this.ctx.state === 'suspended') {\r
6054
- console.log('Resuming AudioContext...');\r
6055
- await this.ctx.resume();\r
6056
- }\r
6057
- \r
6058
- // 1. Get initial stream (activates permissions so labels are visible)\r
6059
- console.log('Requesting initial getUserMedia...');\r
6060
- let stream = await navigator.mediaDevices.getUserMedia({ audio: true });\r
6061
- console.log('Initial stream obtained:', stream.id);\r
6062
- \r
6063
- // 2. Search for preferred device (loopback)\r
6064
- let target = null;\r
6065
- if (preferredLabel) {\r
6066
- const devices = await navigator.mediaDevices.enumerateDevices();\r
6067
- console.log("Audio Devices Found:", devices.length, devices.map(d => \`\${d.label} (\${d.kind})\`));\r
6068
- \r
6069
- target = devices.find(d => d.kind === 'audioinput' &&\r
6070
- d.label.toLowerCase().includes(preferredLabel.toLowerCase()));\r
6071
- }\r
6072
- \r
6073
- if (target) {\r
6074
- console.log(\`Switching audio input to: \${target.label} [\${target.deviceId}]\`);\r
6075
- stream.getTracks().forEach(t => t.stop()); // Stop generic stream\r
6076
- \r
6077
- // Request specific device - being explicit with constraints\r
6078
- stream = await navigator.mediaDevices.getUserMedia({\r
6079
- audio: {\r
6080
- deviceId: { exact: target.deviceId },\r
6081
- echoCancellation: false,\r
6082
- noiseSuppression: false,\r
6083
- autoGainControl: false,\r
6084
- channelCount: 2\r
6085
- }\r
6086
- });\r
6087
- this.currentTrackName = target.label;\r
6088
- } else {\r
6089
- if (preferredLabel) console.warn(\`Preferred device '\${preferredLabel}' not found. Using default.\`);\r
6090
- // Keep the initial stream\r
6091
- this.currentTrackName = stream.getAudioTracks()[0]?.label || 'Live Microphone';\r
6092
- }\r
6093
- \r
6094
- this.stopCurrentSource();\r
6095
- this.micStream = stream;\r
6096
- this.source = this.ctx.createMediaStreamSource(stream);\r
6097
- this.gainNode = this.ctx.createGain();\r
6098
- this.gainNode.gain.value = 3.0; // Boost loopback/mic signal before FFT \u2014 tune via sensitivity slider\r
6099
- this.source.connect(this.gainNode);\r
6100
- this.gainNode.connect(this.analyser);\r
6101
- this.mode = 'mic';\r
6102
- this.isPlaying = true;\r
6103
- this.resetCalibration();\r
6104
- \r
6105
- if (status) status.innerHTML = 'SYSTEM: <span>LISTENING (' + (preferredLabel ? 'Loopback' : 'Mic') + ')</span>';\r
6106
- updateUI();\r
6107
- \r
6108
- return stream;\r
6109
- } catch (e) {\r
6110
- console.error("AUDIO.startMic failed:", e);\r
6111
- throw e;\r
6112
- }\r
6113
- },\r
6114
- \r
6115
- resetCalibration() { this.gateState.isCalibrating = true; this.gateState.calibrationSamples = []; },\r
6116
- \r
6117
- applyBassGate(rawValue) {\r
6118
- if (!STATE.bassGateEnabled) return rawValue;\r
6119
- const { bassGateThreshold: threshold, bassGateAttack: attack, bassGateRelease: release, bassGateRatio: ratio } = STATE;\r
6120
- if (this.gateState.isCalibrating) {\r
6121
- this.gateState.calibrationSamples.push(rawValue);\r
6122
- if (this.gateState.calibrationSamples.length >= this.gateState.calibrationFrames) {\r
6123
- const sorted = [...this.gateState.calibrationSamples].sort((a, b) => a - b);\r
6124
- this.gateState.noiseFloor = sorted.slice(0, Math.floor(sorted.length * 0.2)).reduce((a, b) => a + b, 0) / Math.floor(sorted.length * 0.2);\r
6125
- this.gateState.isCalibrating = false;\r
6126
- }\r
6127
- }\r
6128
- const effectiveThreshold = Math.max(threshold, this.gateState.noiseFloor * 1.5);\r
6129
- if (rawValue > effectiveThreshold) { this.gateState.isOpen = true; this.gateState.holdTime = 10; }\r
6130
- else if (rawValue < effectiveThreshold * 0.7 && this.gateState.holdTime <= 0) { this.gateState.isOpen = false; }\r
6131
- if (this.gateState.holdTime > 0) this.gateState.holdTime--;\r
6132
- this.gateState.envelope += ((this.gateState.isOpen ? 1.0 : 0.0) - this.gateState.envelope) * (this.gateState.isOpen ? attack : release);\r
6133
- let output = rawValue;\r
6134
- if (this.gateState.isOpen && rawValue > effectiveThreshold) {\r
6135
- output = effectiveThreshold + (rawValue - effectiveThreshold) / ratio;\r
6136
- }\r
6137
- return output * this.gateState.envelope;\r
6138
- },\r
6139
- \r
5962
+ // === ENHANCEMENT: Onset Detection ===
5963
+ onsetEnergy: 0, onsetDetected: false, onsetHistory: [], onsetThreshold: 0,
5964
+ // === ENHANCEMENT: Waveform Data ===
5965
+ waveformData: new Float32Array(128), waveformSmoothed: new Float32Array(128),
5966
+ gateState: {
5967
+ isOpen: false, envelope: 0, holdTime: 0, noiseFloor: 0.05,
5968
+ calibrationSamples: [], isCalibrating: true, calibrationFrames: 30
5969
+ },
5970
+
5971
+ init() {
5972
+ if (this.ctx) return;
5973
+ this.ctx = new (window.AudioContext || window.webkitAudioContext)();
5974
+ this.analyser = this.ctx.createAnalyser();
5975
+ this.analyser.fftSize = 4096;
5976
+ this.analyser.smoothingTimeConstant = 0.65;
5977
+ this.data = new Uint8Array(this.analyser.frequencyBinCount);
5978
+ this.prevSpectrum = new Float32Array(this.analyser.frequencyBinCount);
5979
+ this.active = true;
5980
+ },
5981
+
5982
+ loadTrack(arrayBuffer, fileName) {
5983
+ return new Promise((resolve, reject) => {
5984
+ this.ctx.decodeAudioData(arrayBuffer, (buffer) => {
5985
+ this.stopCurrentSource();
5986
+ this.audioBuffer = buffer;
5987
+ this.currentTrackName = fileName || 'Unknown';
5988
+ this.mode = 'file';
5989
+ this.pauseTime = 0;
5990
+ this.resetCalibration();
5991
+ resolve(buffer);
5992
+ }, reject);
5993
+ });
5994
+ },
5995
+
5996
+ play() {
5997
+ if (!this.audioBuffer || this.mode !== 'file' || this.isPlaying) return;
5998
+ this.source = this.ctx.createBufferSource();
5999
+ this.source.buffer = this.audioBuffer;
6000
+ this.source.loop = true;
6001
+ this.source.connect(this.analyser);
6002
+ this.analyser.connect(this.ctx.destination);
6003
+ this.source.start(0, this.pauseTime);
6004
+ this.startTime = this.ctx.currentTime - this.pauseTime;
6005
+ this.isPlaying = true;
6006
+ this.isPaused = false;
6007
+ },
6008
+
6009
+ pause() {
6010
+ if (!this.isPlaying || this.mode !== 'file') return;
6011
+ this.pauseTime = this.getCurrentTime();
6012
+ this.isPaused = true;
6013
+ this.isPlaying = false;
6014
+ if (this.source) { try { this.source.stop(); } catch (e) { } this.source.disconnect(); this.source = null; }
6015
+ },
6016
+
6017
+ togglePlayPause() {
6018
+ if (this.mode === 'mic') return false;
6019
+ if (this.isPlaying) { this.pause(); return false; }
6020
+ else { this.play(); return true; }
6021
+ },
6022
+
6023
+ getCurrentTime() {
6024
+ if (!this.isPlaying) return this.pauseTime;
6025
+ if (!this.audioBuffer) return 0;
6026
+ return (this.ctx.currentTime - this.startTime) % this.audioBuffer.duration;
6027
+ },
6028
+
6029
+ getDuration() { return this.audioBuffer ? this.audioBuffer.duration : 0; },
6030
+
6031
+ seek(time) {
6032
+ if (this.mode !== 'file' || !this.audioBuffer) return;
6033
+ const wasPlaying = this.isPlaying;
6034
+ if (wasPlaying) this.pause();
6035
+ this.pauseTime = Math.max(0, Math.min(time, this.audioBuffer.duration));
6036
+ if (wasPlaying) this.play();
6037
+ },
6038
+
6039
+ stopCurrentSource() {
6040
+ if (this.source) { try { this.source.stop(); this.source.disconnect(); } catch (e) { } this.source = null; }
6041
+ if (this.gainNode) { try { this.gainNode.disconnect(); } catch (e) { } this.gainNode = null; }
6042
+ if (this.micStream) { this.micStream.getTracks().forEach(t => t.stop()); this.micStream = null; }
6043
+ this.isPlaying = false; this.isPaused = false; this.pauseTime = 0;
6044
+ },
6045
+
6046
+ async startMic(preferredLabel = null) {
6047
+ console.log('startMic called with preferredLabel:', preferredLabel);
6048
+ const status = document.getElementById('status-bar');
6049
+ if (status) status.innerHTML = 'SYSTEM: <span>REQUESTING ACCESS...</span>';
6050
+
6051
+ try {
6052
+ // Ensure context is running
6053
+ if (this.ctx.state === 'suspended') {
6054
+ console.log('Resuming AudioContext...');
6055
+ await this.ctx.resume();
6056
+ }
6057
+
6058
+ // 1. Get initial stream (activates permissions so labels are visible)
6059
+ console.log('Requesting initial getUserMedia...');
6060
+ let stream = await navigator.mediaDevices.getUserMedia({ audio: true });
6061
+ console.log('Initial stream obtained:', stream.id);
6062
+
6063
+ // 2. Search for preferred device (loopback)
6064
+ let target = null;
6065
+ if (preferredLabel) {
6066
+ const devices = await navigator.mediaDevices.enumerateDevices();
6067
+ console.log("Audio Devices Found:", devices.length, devices.map(d => \`\${d.label} (\${d.kind})\`));
6068
+
6069
+ target = devices.find(d => d.kind === 'audioinput' &&
6070
+ d.label.toLowerCase().includes(preferredLabel.toLowerCase()));
6071
+ }
6072
+
6073
+ if (target) {
6074
+ console.log(\`Switching audio input to: \${target.label} [\${target.deviceId}]\`);
6075
+ stream.getTracks().forEach(t => t.stop()); // Stop generic stream
6076
+
6077
+ // Request specific device - being explicit with constraints
6078
+ stream = await navigator.mediaDevices.getUserMedia({
6079
+ audio: {
6080
+ deviceId: { exact: target.deviceId },
6081
+ echoCancellation: false,
6082
+ noiseSuppression: false,
6083
+ autoGainControl: false,
6084
+ channelCount: 2
6085
+ }
6086
+ });
6087
+ this.currentTrackName = target.label;
6088
+ } else {
6089
+ if (preferredLabel) console.warn(\`Preferred device '\${preferredLabel}' not found. Using default.\`);
6090
+ // Keep the initial stream
6091
+ this.currentTrackName = stream.getAudioTracks()[0]?.label || 'Live Microphone';
6092
+ }
6093
+
6094
+ this.stopCurrentSource();
6095
+ this.micStream = stream;
6096
+ this.source = this.ctx.createMediaStreamSource(stream);
6097
+ this.gainNode = this.ctx.createGain();
6098
+ this.gainNode.gain.value = 3.0; // Boost loopback/mic signal before FFT \u2014 tune via sensitivity slider
6099
+ this.source.connect(this.gainNode);
6100
+ this.gainNode.connect(this.analyser);
6101
+ this.mode = 'mic';
6102
+ this.isPlaying = true;
6103
+ this.resetCalibration();
6104
+
6105
+ if (status) status.innerHTML = 'SYSTEM: <span>LISTENING (' + (preferredLabel ? 'Loopback' : 'Mic') + ')</span>';
6106
+ updateUI();
6107
+
6108
+ return stream;
6109
+ } catch (e) {
6110
+ console.error("AUDIO.startMic failed:", e);
6111
+ throw e;
6112
+ }
6113
+ },
6114
+
6115
+ resetCalibration() { this.gateState.isCalibrating = true; this.gateState.calibrationSamples = []; },
6116
+
6117
+ applyBassGate(rawValue) {
6118
+ if (!STATE.bassGateEnabled) return rawValue;
6119
+ const { bassGateThreshold: threshold, bassGateAttack: attack, bassGateRelease: release, bassGateRatio: ratio } = STATE;
6120
+ if (this.gateState.isCalibrating) {
6121
+ this.gateState.calibrationSamples.push(rawValue);
6122
+ if (this.gateState.calibrationSamples.length >= this.gateState.calibrationFrames) {
6123
+ const sorted = [...this.gateState.calibrationSamples].sort((a, b) => a - b);
6124
+ this.gateState.noiseFloor = sorted.slice(0, Math.floor(sorted.length * 0.2)).reduce((a, b) => a + b, 0) / Math.floor(sorted.length * 0.2);
6125
+ this.gateState.isCalibrating = false;
6126
+ }
6127
+ }
6128
+ const effectiveThreshold = Math.max(threshold, this.gateState.noiseFloor * 1.5);
6129
+ if (rawValue > effectiveThreshold) { this.gateState.isOpen = true; this.gateState.holdTime = 10; }
6130
+ else if (rawValue < effectiveThreshold * 0.7 && this.gateState.holdTime <= 0) { this.gateState.isOpen = false; }
6131
+ if (this.gateState.holdTime > 0) this.gateState.holdTime--;
6132
+ this.gateState.envelope += ((this.gateState.isOpen ? 1.0 : 0.0) - this.gateState.envelope) * (this.gateState.isOpen ? attack : release);
6133
+ let output = rawValue;
6134
+ if (this.gateState.isOpen && rawValue > effectiveThreshold) {
6135
+ output = effectiveThreshold + (rawValue - effectiveThreshold) / ratio;
6136
+ }
6137
+ return output * this.gateState.envelope;
6138
+ },
6139
+
6140
6140
  update() {
6141
6141
  if (!this.active) return;
6142
6142
  if (!this.isPlaying || !this.source) {
@@ -6152,127 +6152,127 @@ var reference_default = `<!DOCTYPE html>
6152
6152
  }
6153
6153
  this.idleCleared = false;
6154
6154
  this.analyser.getByteFrequencyData(this.data);
6155
- const len = this.data.length, nyquist = this.ctx.sampleRate / 2;\r
6156
- const getFreqIndex = (freq) => Math.round(freq / nyquist * len);\r
6157
- const ranges = { subBass: [50, 120], bass: [120, 200], lowMid: [200, 450], mid: [450, 2000], highMid: [2000, 3500], high: [3500, 12000], ultraHigh: [12000, 20000] };\r
6158
- let totalEnergy = 0, weightedFreqSum = 0;\r
6159
- for (const [band, [low, high]] of Object.entries(ranges)) {\r
6160
- const lowIdx = getFreqIndex(low), highIdx = getFreqIndex(high);\r
6161
- let sum = 0;\r
6162
- for (let i = lowIdx; i < highIdx && i < len; i++) {\r
6163
- const v = this.data[i] / 255.0;\r
6164
- sum += v; totalEnergy += v; weightedFreqSum += v * i;\r
6165
- }\r
6166
- const rawBand = sum / (highIdx - lowIdx);\r
6167
- this.bands[band] = rawBand;\r
6168
- this.gatedBands[band] = (band === 'subBass' || band === 'bass' || band === 'lowMid') ? this.applyBassGate(rawBand) : rawBand;\r
6169
- }\r
6170
- this.spectralCentroid = totalEnergy > 0 ? weightedFreqSum / totalEnergy / len : 0;\r
6171
- let flux = 0;\r
6172
- for (let i = 0; i < len; i++) {\r
6173
- const current = this.data[i] / 255.0, diff = current - this.prevSpectrum[i];\r
6174
- flux += diff > 0 ? diff * diff : 0;\r
6175
- this.prevSpectrum[i] = current;\r
6176
- }\r
6177
- this.spectralFlux = Math.sqrt(flux / len);\r
6178
- const currentEnergy = this.gatedBands.bass * 0.6 + this.gatedBands.subBass * 0.4;\r
6179
- this.peakHistory.push(currentEnergy);\r
6180
- if (this.peakHistory.length > 50) this.peakHistory.shift();\r
6181
- const avgEnergy = this.peakHistory.reduce((a, b) => a + b, 0) / this.peakHistory.length;\r
6182
- const variance = this.peakHistory.reduce((a, b) => a + Math.pow(b - avgEnergy, 2), 0) / this.peakHistory.length;\r
6183
- const dynamicThreshold = avgEnergy + Math.sqrt(variance) * 1.8;\r
6184
- const now = performance.now();\r
6185
- if (currentEnergy > dynamicThreshold && currentEnergy > this.beatThreshold && now - this.lastBeatTime > 120) {\r
6186
- this.beatDetected = true; this.beatEnergy = 1.0; this.lastBeatTime = now;\r
6187
- } else { this.beatDetected = false; }\r
6188
- this.beatEnergy *= this.beatDecay;\r
6189
- \r
6190
- // === ENHANCEMENT: Improved Onset Detection ===\r
6191
- // Uses spectral flux derivative for sharper transient response\r
6192
- const onsetSens = STATE.onsetSensitivity;\r
6193
- this.onsetHistory.push(this.spectralFlux);\r
6194
- if (this.onsetHistory.length > 8) this.onsetHistory.shift();\r
6195
- const recentAvg = this.onsetHistory.slice(-4).reduce((a, b) => a + b, 0) / 4;\r
6196
- const olderAvg = this.onsetHistory.slice(0, 4).reduce((a, b) => a + b, 0) / Math.max(1, this.onsetHistory.slice(0, 4).length);\r
6197
- const onsetDelta = (recentAvg - olderAvg) * onsetSens;\r
6198
- this.onsetDetected = onsetDelta > 0.015 && now - this.lastBeatTime > 50;\r
6199
- this.onsetEnergy = this.onsetDetected ? 1.0 : this.onsetEnergy * STATE.onsetDecay;\r
6200
- \r
6201
- // === ENHANCEMENT: Waveform Data for Terrain Field ===\r
6202
- const waveformBins = 128;\r
6203
- const binSize = Math.floor(len / waveformBins);\r
6204
- for (let i = 0; i < waveformBins; i++) {\r
6205
- let sum = 0;\r
6206
- for (let j = 0; j < binSize; j++) {\r
6207
- sum += this.data[i * binSize + j] / 255.0;\r
6208
- }\r
6209
- const target = sum / binSize;\r
6210
- this.waveformSmoothed[i] += (target - this.waveformSmoothed[i]) * 0.3;\r
6211
- this.waveformData[i] = this.waveformSmoothed[i];\r
6212
- }\r
6213
- }\r
6214
- };\r
6215
- \r
6216
- // --- SCENE SETUP ---\r
6217
- const scene = new THREE.Scene();\r
6218
- scene.fog = new THREE.FogExp2(0x000508, 0.005);\r
6219
- const camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 0.1, 800);\r
6220
- camera.position.set(0, 12, CONFIG.defaultZoom);\r
6221
- const cameraShakeOffset = new THREE.Vector3();\r
6222
- const baseCameraPos = camera.position.clone();\r
6223
- const zoomIndicator = document.getElementById('zoom-level');\r
6224
- \r
6155
+ const len = this.data.length, nyquist = this.ctx.sampleRate / 2;
6156
+ const getFreqIndex = (freq) => Math.round(freq / nyquist * len);
6157
+ const ranges = { subBass: [50, 120], bass: [120, 200], lowMid: [200, 450], mid: [450, 2000], highMid: [2000, 3500], high: [3500, 12000], ultraHigh: [12000, 20000] };
6158
+ let totalEnergy = 0, weightedFreqSum = 0;
6159
+ for (const [band, [low, high]] of Object.entries(ranges)) {
6160
+ const lowIdx = getFreqIndex(low), highIdx = getFreqIndex(high);
6161
+ let sum = 0;
6162
+ for (let i = lowIdx; i < highIdx && i < len; i++) {
6163
+ const v = this.data[i] / 255.0;
6164
+ sum += v; totalEnergy += v; weightedFreqSum += v * i;
6165
+ }
6166
+ const rawBand = sum / (highIdx - lowIdx);
6167
+ this.bands[band] = rawBand;
6168
+ this.gatedBands[band] = (band === 'subBass' || band === 'bass' || band === 'lowMid') ? this.applyBassGate(rawBand) : rawBand;
6169
+ }
6170
+ this.spectralCentroid = totalEnergy > 0 ? weightedFreqSum / totalEnergy / len : 0;
6171
+ let flux = 0;
6172
+ for (let i = 0; i < len; i++) {
6173
+ const current = this.data[i] / 255.0, diff = current - this.prevSpectrum[i];
6174
+ flux += diff > 0 ? diff * diff : 0;
6175
+ this.prevSpectrum[i] = current;
6176
+ }
6177
+ this.spectralFlux = Math.sqrt(flux / len);
6178
+ const currentEnergy = this.gatedBands.bass * 0.6 + this.gatedBands.subBass * 0.4;
6179
+ this.peakHistory.push(currentEnergy);
6180
+ if (this.peakHistory.length > 50) this.peakHistory.shift();
6181
+ const avgEnergy = this.peakHistory.reduce((a, b) => a + b, 0) / this.peakHistory.length;
6182
+ const variance = this.peakHistory.reduce((a, b) => a + Math.pow(b - avgEnergy, 2), 0) / this.peakHistory.length;
6183
+ const dynamicThreshold = avgEnergy + Math.sqrt(variance) * 1.8;
6184
+ const now = performance.now();
6185
+ if (currentEnergy > dynamicThreshold && currentEnergy > this.beatThreshold && now - this.lastBeatTime > 120) {
6186
+ this.beatDetected = true; this.beatEnergy = 1.0; this.lastBeatTime = now;
6187
+ } else { this.beatDetected = false; }
6188
+ this.beatEnergy *= this.beatDecay;
6189
+
6190
+ // === ENHANCEMENT: Improved Onset Detection ===
6191
+ // Uses spectral flux derivative for sharper transient response
6192
+ const onsetSens = STATE.onsetSensitivity;
6193
+ this.onsetHistory.push(this.spectralFlux);
6194
+ if (this.onsetHistory.length > 8) this.onsetHistory.shift();
6195
+ const recentAvg = this.onsetHistory.slice(-4).reduce((a, b) => a + b, 0) / 4;
6196
+ const olderAvg = this.onsetHistory.slice(0, 4).reduce((a, b) => a + b, 0) / Math.max(1, this.onsetHistory.slice(0, 4).length);
6197
+ const onsetDelta = (recentAvg - olderAvg) * onsetSens;
6198
+ this.onsetDetected = onsetDelta > 0.015 && now - this.lastBeatTime > 50;
6199
+ this.onsetEnergy = this.onsetDetected ? 1.0 : this.onsetEnergy * STATE.onsetDecay;
6200
+
6201
+ // === ENHANCEMENT: Waveform Data for Terrain Field ===
6202
+ const waveformBins = 128;
6203
+ const binSize = Math.floor(len / waveformBins);
6204
+ for (let i = 0; i < waveformBins; i++) {
6205
+ let sum = 0;
6206
+ for (let j = 0; j < binSize; j++) {
6207
+ sum += this.data[i * binSize + j] / 255.0;
6208
+ }
6209
+ const target = sum / binSize;
6210
+ this.waveformSmoothed[i] += (target - this.waveformSmoothed[i]) * 0.3;
6211
+ this.waveformData[i] = this.waveformSmoothed[i];
6212
+ }
6213
+ }
6214
+ };
6215
+
6216
+ // --- SCENE SETUP ---
6217
+ const scene = new THREE.Scene();
6218
+ scene.fog = new THREE.FogExp2(0x000508, 0.005);
6219
+ const camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 0.1, 800);
6220
+ camera.position.set(0, 12, CONFIG.defaultZoom);
6221
+ const cameraShakeOffset = new THREE.Vector3();
6222
+ const baseCameraPos = camera.position.clone();
6223
+ const zoomIndicator = document.getElementById('zoom-level');
6224
+
6225
6225
  const renderer = new THREE.WebGLRenderer({ powerPreference: "high-performance", antialias: true, alpha: true });
6226
6226
  renderer.setClearColor(0x000000, 0);
6227
- renderer.setSize(window.innerWidth, window.innerHeight);\r
6228
- renderer.setPixelRatio(STATE.pixelRatio);\r
6229
- renderer.toneMapping = THREE.ACESFilmicToneMapping;\r
6230
- renderer.toneMappingExposure = 2.1;\r
6231
- document.body.appendChild(renderer.domElement);\r
6232
- \r
6233
- const controls = new OrbitControls(camera, renderer.domElement);\r
6234
- controls.enableDamping = true;\r
6235
- controls.dampingFactor = 0.06;\r
6227
+ renderer.setSize(window.innerWidth, window.innerHeight);
6228
+ renderer.setPixelRatio(STATE.pixelRatio);
6229
+ renderer.toneMapping = THREE.ACESFilmicToneMapping;
6230
+ renderer.toneMappingExposure = 2.1;
6231
+ document.body.appendChild(renderer.domElement);
6232
+
6233
+ const controls = new OrbitControls(camera, renderer.domElement);
6234
+ controls.enableDamping = true;
6235
+ controls.dampingFactor = 0.06;
6236
6236
  controls.autoRotate = false;
6237
- controls.autoRotateSpeed = 0.3;\r
6238
- controls.minDistance = CONFIG.minZoom;\r
6239
- controls.maxDistance = CONFIG.maxZoom;\r
6240
- controls.maxPolarAngle = Math.PI * 0.85;\r
6241
- controls.minPolarAngle = Math.PI * 0.15;\r
6242
- \r
6243
- controls.addEventListener('change', () => {\r
6244
- const dist = camera.position.length();\r
6245
- const zoomPercent = Math.round((1 - (dist - CONFIG.minZoom) / (CONFIG.maxZoom - CONFIG.minZoom)) * 100);\r
6246
- zoomIndicator.textContent = Math.max(0, Math.min(100, zoomPercent)) + '%';\r
6247
- baseCameraPos.copy(camera.position);\r
6248
- });\r
6249
- \r
6250
- const raycaster = new THREE.Raycaster();\r
6251
- const mouse = new THREE.Vector2(9999, 9999);\r
6252
- const plane = new THREE.Plane(new THREE.Vector3(0, 0, 1), 0);\r
6253
- const mouseWorldPos = new THREE.Vector3();\r
6254
- let mouseVelocity = new THREE.Vector2();\r
6255
- let prevMouse = new THREE.Vector2();\r
6256
- \r
6257
- window.addEventListener('mousemove', (e) => {\r
6258
- const newMouse = new THREE.Vector2((e.clientX / window.innerWidth) * 2 - 1, -(e.clientY / window.innerHeight) * 2 + 1);\r
6259
- mouseVelocity.subVectors(newMouse, prevMouse);\r
6260
- prevMouse.copy(newMouse);\r
6261
- mouse.copy(newMouse);\r
6262
- raycaster.setFromCamera(mouse, camera);\r
6263
- const target = new THREE.Vector3();\r
6264
- raycaster.ray.intersectPlane(plane, target);\r
6265
- if (target) mouseWorldPos.copy(target);\r
6266
- });\r
6267
- \r
6268
- window.addEventListener('keydown', (e) => {\r
6269
- if (e.key === '+' || e.key === '=') { camera.position.multiplyScalar(0.9); controls.update(); }\r
6270
- else if (e.key === '-' || e.key === '_') { camera.position.multiplyScalar(1.1); controls.update(); }\r
6271
- else if (e.key === '0') { camera.position.normalize().multiplyScalar(CONFIG.defaultZoom); controls.update(); }\r
6272
- else if (e.key === ' ') { e.preventDefault(); togglePlayPause(); }\r
6237
+ controls.autoRotateSpeed = 0.3;
6238
+ controls.minDistance = CONFIG.minZoom;
6239
+ controls.maxDistance = CONFIG.maxZoom;
6240
+ controls.maxPolarAngle = Math.PI * 0.85;
6241
+ controls.minPolarAngle = Math.PI * 0.15;
6242
+
6243
+ controls.addEventListener('change', () => {
6244
+ const dist = camera.position.length();
6245
+ const zoomPercent = Math.round((1 - (dist - CONFIG.minZoom) / (CONFIG.maxZoom - CONFIG.minZoom)) * 100);
6246
+ zoomIndicator.textContent = Math.max(0, Math.min(100, zoomPercent)) + '%';
6247
+ baseCameraPos.copy(camera.position);
6248
+ });
6249
+
6250
+ const raycaster = new THREE.Raycaster();
6251
+ const mouse = new THREE.Vector2(9999, 9999);
6252
+ const plane = new THREE.Plane(new THREE.Vector3(0, 0, 1), 0);
6253
+ const mouseWorldPos = new THREE.Vector3();
6254
+ let mouseVelocity = new THREE.Vector2();
6255
+ let prevMouse = new THREE.Vector2();
6256
+
6257
+ window.addEventListener('mousemove', (e) => {
6258
+ const newMouse = new THREE.Vector2((e.clientX / window.innerWidth) * 2 - 1, -(e.clientY / window.innerHeight) * 2 + 1);
6259
+ mouseVelocity.subVectors(newMouse, prevMouse);
6260
+ prevMouse.copy(newMouse);
6261
+ mouse.copy(newMouse);
6262
+ raycaster.setFromCamera(mouse, camera);
6263
+ const target = new THREE.Vector3();
6264
+ raycaster.ray.intersectPlane(plane, target);
6265
+ if (target) mouseWorldPos.copy(target);
6266
+ });
6267
+
6268
+ window.addEventListener('keydown', (e) => {
6269
+ if (e.key === '+' || e.key === '=') { camera.position.multiplyScalar(0.9); controls.update(); }
6270
+ else if (e.key === '-' || e.key === '_') { camera.position.multiplyScalar(1.1); controls.update(); }
6271
+ else if (e.key === '0') { camera.position.normalize().multiplyScalar(CONFIG.defaultZoom); controls.update(); }
6272
+ else if (e.key === ' ') { e.preventDefault(); togglePlayPause(); }
6273
6273
  else if (e.key === 'f' || e.key === 'F') { STATE.dofEnabled = !STATE.dofEnabled; customDOFPass.enabled = STATE.dofEnabled; }
6274
- });\r
6275
- \r
6274
+ });
6275
+
6276
6276
  // Every field type shares one screen-fitted footprint; switching presets never reallocates particles.
6277
6277
  const shapeFunction = \`
6278
6278
  vec3 shapedPosition(vec3 seed) {
@@ -6286,100 +6286,100 @@ vec3 shapedPosition(vec3 seed) {
6286
6286
  const vertexShader = \`
6287
6287
  uniform float uTime, uPixelRatio, uSizeBase, uNoiseScale, uCurlStrength;
6288
6288
  uniform float uViewportAspect, uViewportHalfHeight;
6289
- uniform vec3 uColor1, uColor2, uColor3;\r
6290
- uniform float uSubBass, uBass, uLowMid, uMid, uHighMid, uHigh, uUltraHigh;\r
6291
- uniform float uBeatEnergy, uSpectralCentroid, uSpectralFlux;\r
6292
- uniform float uOnsetEnergy, uTerrainMode, uTerrainHeight;\r
6293
- uniform float uVortexStrength, uPulseIntensity, uZoomFactor;\r
6294
- uniform vec3 uMousePos;\r
6295
- uniform vec2 uMouseVelocity;\r
6296
- attribute vec3 aRandom;\r
6297
- attribute float aPhase, aLayer;\r
6298
- varying vec3 vColor;\r
6289
+ uniform vec3 uColor1, uColor2, uColor3;
6290
+ uniform float uSubBass, uBass, uLowMid, uMid, uHighMid, uHigh, uUltraHigh;
6291
+ uniform float uBeatEnergy, uSpectralCentroid, uSpectralFlux;
6292
+ uniform float uOnsetEnergy, uTerrainMode, uTerrainHeight;
6293
+ uniform float uVortexStrength, uPulseIntensity, uZoomFactor;
6294
+ uniform vec3 uMousePos;
6295
+ uniform vec2 uMouseVelocity;
6296
+ attribute vec3 aRandom;
6297
+ attribute float aPhase, aLayer;
6298
+ varying vec3 vColor;
6299
6299
  varying float vAlpha, vEnergy, vDepth, vRimLight, vGlint;
6300
- \r
6301
- vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }\r
6302
- vec4 mod289(vec4 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }\r
6303
- vec4 permute(vec4 x) { return mod289(((x*34.0)+1.0)*x); }\r
6304
- vec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; }\r
6305
- \r
6306
- float snoise(vec3 v) {\r
6307
- const vec2 C = vec2(1.0/6.0, 1.0/3.0);\r
6308
- const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);\r
6309
- vec3 i = floor(v + dot(v, C.yyy));\r
6310
- vec3 x0 = v - i + dot(i, C.xxx);\r
6311
- vec3 g = step(x0.yzx, x0.xyz);\r
6312
- vec3 l = 1.0 - g;\r
6313
- vec3 i1 = min(g.xyz, l.zxy);\r
6314
- vec3 i2 = max(g.xyz, l.zxy);\r
6315
- vec3 x1 = x0 - i1 + C.xxx;\r
6316
- vec3 x2 = x0 - i2 + C.yyy;\r
6317
- vec3 x3 = x0 - D.yyy;\r
6318
- i = mod289(i);\r
6319
- vec4 p = permute(permute(permute(i.z + vec4(0.0, i1.z, i2.z, 1.0)) + i.y + vec4(0.0, i1.y, i2.y, 1.0)) + i.x + vec4(0.0, i1.x, i2.x, 1.0));\r
6320
- float n_ = 0.142857142857;\r
6321
- vec3 ns = n_ * D.wyz - D.xzx;\r
6322
- vec4 j = p - 49.0 * floor(p * ns.z * ns.z);\r
6323
- vec4 x_ = floor(j * ns.z);\r
6324
- vec4 y_ = floor(j - 7.0 * x_);\r
6325
- vec4 x = x_ * ns.x + ns.yyyy;\r
6326
- vec4 y = y_ * ns.x + ns.yyyy;\r
6327
- vec4 h = 1.0 - abs(x) - abs(y);\r
6328
- vec4 b0 = vec4(x.xy, y.xy);\r
6329
- vec4 b1 = vec4(x.zw, y.zw);\r
6330
- vec4 s0 = floor(b0) * 2.0 + 1.0;\r
6331
- vec4 s1 = floor(b1) * 2.0 + 1.0;\r
6332
- vec4 sh = -step(h, vec4(0.0));\r
6333
- vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy;\r
6334
- vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww;\r
6335
- vec3 p0 = vec3(a0.xy, h.x);\r
6336
- vec3 p1 = vec3(a0.zw, h.y);\r
6337
- vec3 p2 = vec3(a1.xy, h.z);\r
6338
- vec3 p3 = vec3(a1.zw, h.w);\r
6339
- vec4 norm = taylorInvSqrt(vec4(dot(p0, p0), dot(p1, p1), dot(p2, p2), dot(p3, p3)));\r
6340
- p0 *= norm.x; p1 *= norm.y; p2 *= norm.z; p3 *= norm.w;\r
6341
- vec4 m = max(0.6 - vec4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), 0.0);\r
6342
- m = m * m;\r
6343
- return 42.0 * dot(m * m, vec4(dot(p0, x0), dot(p1, x1), dot(p2, x2), dot(p3, x3)));\r
6344
- }\r
6345
- \r
6300
+
6301
+ vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
6302
+ vec4 mod289(vec4 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
6303
+ vec4 permute(vec4 x) { return mod289(((x*34.0)+1.0)*x); }
6304
+ vec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; }
6305
+
6306
+ float snoise(vec3 v) {
6307
+ const vec2 C = vec2(1.0/6.0, 1.0/3.0);
6308
+ const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
6309
+ vec3 i = floor(v + dot(v, C.yyy));
6310
+ vec3 x0 = v - i + dot(i, C.xxx);
6311
+ vec3 g = step(x0.yzx, x0.xyz);
6312
+ vec3 l = 1.0 - g;
6313
+ vec3 i1 = min(g.xyz, l.zxy);
6314
+ vec3 i2 = max(g.xyz, l.zxy);
6315
+ vec3 x1 = x0 - i1 + C.xxx;
6316
+ vec3 x2 = x0 - i2 + C.yyy;
6317
+ vec3 x3 = x0 - D.yyy;
6318
+ i = mod289(i);
6319
+ vec4 p = permute(permute(permute(i.z + vec4(0.0, i1.z, i2.z, 1.0)) + i.y + vec4(0.0, i1.y, i2.y, 1.0)) + i.x + vec4(0.0, i1.x, i2.x, 1.0));
6320
+ float n_ = 0.142857142857;
6321
+ vec3 ns = n_ * D.wyz - D.xzx;
6322
+ vec4 j = p - 49.0 * floor(p * ns.z * ns.z);
6323
+ vec4 x_ = floor(j * ns.z);
6324
+ vec4 y_ = floor(j - 7.0 * x_);
6325
+ vec4 x = x_ * ns.x + ns.yyyy;
6326
+ vec4 y = y_ * ns.x + ns.yyyy;
6327
+ vec4 h = 1.0 - abs(x) - abs(y);
6328
+ vec4 b0 = vec4(x.xy, y.xy);
6329
+ vec4 b1 = vec4(x.zw, y.zw);
6330
+ vec4 s0 = floor(b0) * 2.0 + 1.0;
6331
+ vec4 s1 = floor(b1) * 2.0 + 1.0;
6332
+ vec4 sh = -step(h, vec4(0.0));
6333
+ vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy;
6334
+ vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww;
6335
+ vec3 p0 = vec3(a0.xy, h.x);
6336
+ vec3 p1 = vec3(a0.zw, h.y);
6337
+ vec3 p2 = vec3(a1.xy, h.z);
6338
+ vec3 p3 = vec3(a1.zw, h.w);
6339
+ vec4 norm = taylorInvSqrt(vec4(dot(p0, p0), dot(p1, p1), dot(p2, p2), dot(p3, p3)));
6340
+ p0 *= norm.x; p1 *= norm.y; p2 *= norm.z; p3 *= norm.w;
6341
+ vec4 m = max(0.6 - vec4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), 0.0);
6342
+ m = m * m;
6343
+ return 42.0 * dot(m * m, vec4(dot(p0, x0), dot(p1, x1), dot(p2, x2), dot(p3, x3)));
6344
+ }
6345
+
6346
6346
  vec3 curl(float x, float y, float z) {
6347
- float eps = 0.08;\r
6348
- return vec3(\r
6349
- snoise(vec3(x, y+eps, z)) - snoise(vec3(x, y-eps, z)),\r
6350
- snoise(vec3(x, y, z+eps)) - snoise(vec3(x, y, z-eps)),\r
6351
- snoise(vec3(x+eps, y, z)) - snoise(vec3(x-eps, y, z))\r
6352
- );\r
6347
+ float eps = 0.08;
6348
+ return vec3(
6349
+ snoise(vec3(x, y+eps, z)) - snoise(vec3(x, y-eps, z)),
6350
+ snoise(vec3(x, y, z+eps)) - snoise(vec3(x, y, z-eps)),
6351
+ snoise(vec3(x+eps, y, z)) - snoise(vec3(x-eps, y, z))
6352
+ );
6353
6353
  }
6354
6354
 
6355
6355
  \${shapeFunction}
6356
6356
 
6357
6357
  void main() {
6358
- float t = uTime * (1.0 + uMid * 0.5);\r
6359
- vec3 noisePos = position * 0.035 + aRandom;\r
6360
- float layerMod = 1.0 + aLayer * 0.5;\r
6361
- \r
6362
- vec3 flow1 = curl(noisePos.x * uNoiseScale + t*0.1, noisePos.y * uNoiseScale, noisePos.z * uNoiseScale);\r
6363
- vec3 flow2 = curl(noisePos.x * uNoiseScale * 2.2 + t*0.07, noisePos.y * uNoiseScale * 2.2 + t*0.08, noisePos.z * uNoiseScale * 2.2) * 0.4;\r
6364
- vec3 flow3 = curl(noisePos.x * uNoiseScale * 0.5 - t*0.05, noisePos.y * uNoiseScale * 0.5, noisePos.z * uNoiseScale * 0.5) * 0.6 * uLowMid;\r
6365
- vec3 flow = flow1 + flow2 * uHigh + flow3;\r
6366
- \r
6358
+ float t = uTime * (1.0 + uMid * 0.5);
6359
+ vec3 noisePos = position * 0.035 + aRandom;
6360
+ float layerMod = 1.0 + aLayer * 0.5;
6361
+
6362
+ vec3 flow1 = curl(noisePos.x * uNoiseScale + t*0.1, noisePos.y * uNoiseScale, noisePos.z * uNoiseScale);
6363
+ vec3 flow2 = curl(noisePos.x * uNoiseScale * 2.2 + t*0.07, noisePos.y * uNoiseScale * 2.2 + t*0.08, noisePos.z * uNoiseScale * 2.2) * 0.4;
6364
+ vec3 flow3 = curl(noisePos.x * uNoiseScale * 0.5 - t*0.05, noisePos.y * uNoiseScale * 0.5, noisePos.z * uNoiseScale * 0.5) * 0.6 * uLowMid;
6365
+ vec3 flow = flow1 + flow2 * uHigh + flow3;
6366
+
6367
6367
  vec3 newPos = shapedPosition(aRandom) + (flow * uCurlStrength * layerMod * (1.0 + uSpectralFlux * 2.5));
6368
- \r
6368
+
6369
6369
  float vortexAngle = uVortexStrength * uBass * 0.45 * (0.75 + aRandom.z * 0.5);
6370
- float cosA = cos(vortexAngle), sinA = sin(vortexAngle);\r
6371
- vec2 rotated = vec2(newPos.x * cosA - newPos.z * sinA, newPos.x * sinA + newPos.z * cosA);\r
6372
- newPos.xz = mix(newPos.xz, rotated, uBass * uVortexStrength);\r
6373
- \r
6374
- float yVortex = sin(newPos.y * 0.1 + uTime) * uMid * uVortexStrength * 2.0;\r
6375
- newPos.x += cos(newPos.y * 0.15) * yVortex;\r
6376
- newPos.z += sin(newPos.y * 0.15) * yVortex;\r
6377
- \r
6378
- float distToMouse = distance(newPos, uMousePos);\r
6379
- float repulsion = smoothstep(25.0, 0.0, distToMouse);\r
6380
- vec3 repelDir = normalize(newPos - uMousePos + vec3(0.001));\r
6381
- newPos += repelDir * repulsion * (12.0 + length(uMouseVelocity) * 360.0);\r
6382
- \r
6370
+ float cosA = cos(vortexAngle), sinA = sin(vortexAngle);
6371
+ vec2 rotated = vec2(newPos.x * cosA - newPos.z * sinA, newPos.x * sinA + newPos.z * cosA);
6372
+ newPos.xz = mix(newPos.xz, rotated, uBass * uVortexStrength);
6373
+
6374
+ float yVortex = sin(newPos.y * 0.1 + uTime) * uMid * uVortexStrength * 2.0;
6375
+ newPos.x += cos(newPos.y * 0.15) * yVortex;
6376
+ newPos.z += sin(newPos.y * 0.15) * yVortex;
6377
+
6378
+ float distToMouse = distance(newPos, uMousePos);
6379
+ float repulsion = smoothstep(25.0, 0.0, distToMouse);
6380
+ vec3 repelDir = normalize(newPos - uMousePos + vec3(0.001));
6381
+ newPos += repelDir * repulsion * (12.0 + length(uMouseVelocity) * 360.0);
6382
+
6383
6383
  // Audio changes depth throughout the plane, without evacuating its centre.
6384
6384
  float beatWave = sin(aRandom.x * 12.0 + aRandom.y * 7.0 - uTime * 6.0);
6385
6385
  float bassDepth = (beatWave * uBeatEnergy * uPulseIntensity * 3.5
@@ -6389,7 +6389,7 @@ void main() {
6389
6389
  newPos.z += bassDepth;
6390
6390
  float fieldWave = sin(aRandom.x * 11.0 - uTime * 0.8 + sin(aRandom.y * 7.0 + uTime * 0.35));
6391
6391
  newPos.z += fieldWave * (1.8 + uBass * 2.4 + uBeatEnergy * 1.6);
6392
- \r
6392
+
6393
6393
  // Waveform relief moves toward the camera while retaining the shared rectangular footprint.
6394
6394
  if (uTerrainMode > 0.5) {
6395
6395
  float freqBin = aRandom.x;
@@ -6399,329 +6399,329 @@ void main() {
6399
6399
  waveHeight *= (1.0 + uSpectralFlux * 3.0);
6400
6400
  newPos.z += waveHeight * uTerrainHeight;
6401
6401
  newPos.z += sin(aRandom.y * 6.28 * 4.0 + uTime) * uSubBass * 5.0;
6402
- }\r
6403
- \r
6404
- vec4 mvPosition = modelViewMatrix * vec4(newPos, 1.0);\r
6405
- gl_Position = projectionMatrix * mvPosition;\r
6406
- \r
6402
+ }
6403
+
6404
+ vec4 mvPosition = modelViewMatrix * vec4(newPos, 1.0);
6405
+ gl_Position = projectionMatrix * mvPosition;
6406
+
6407
6407
  float size = uSizeBase * (0.85 + uBass * 0.5 + uBeatEnergy * 0.5) * (1.0 + (uZoomFactor - 1.0) * 0.3);
6408
6408
  size *= 1.0 + sin(uTime * 10.0 + aPhase * 6.28) * uHigh * 0.35;
6409
6409
  vGlint = smoothstep(0.985, 0.998, aRandom.z) * (0.7 + 0.3 * sin(uTime * 3.0 + aPhase * 6.28)) * (0.75 + uHigh * 0.15 + uOnsetEnergy * 0.1);
6410
6410
  size *= 0.85 + vGlint * 2.1;
6411
6411
  gl_PointSize = clamp(max(size * uPixelRatio * (90.0 / -mvPosition.z) * layerMod, vGlint * 4.5 * uPixelRatio), 0.5, 120.0);
6412
- \r
6413
- float colorMix = smoothstep(0.0, 2.8, length(flow) + uHighMid * 3.0);\r
6414
- vec3 baseColor = mix(uColor1, uColor2, colorMix);\r
6415
- baseColor = mix(baseColor, uColor3, uSpectralCentroid * uHigh * 2.5);\r
6416
- \r
6417
- float energyLevel = uBeatEnergy * 0.35 + repulsion * uMid * 2.0 + uSpectralFlux * 0.8;\r
6418
- vColor = mix(baseColor, vec3(1.0), energyLevel * 0.4);\r
6419
- vColor += vec3(0.08, 0.04, 0.12) * uUltraHigh * 2.5;\r
6420
- \r
6421
- vRimLight = pow(1.0 - max(0.0, dot(normalize(-mvPosition.xyz), normalize(newPos))), 3.0);\r
6422
- vEnergy = energyLevel;\r
6423
- vDepth = clamp(-mvPosition.z / 120.0, 0.0, 1.0);\r
6424
- \r
6412
+
6413
+ float colorMix = smoothstep(0.0, 2.8, length(flow) + uHighMid * 3.0);
6414
+ vec3 baseColor = mix(uColor1, uColor2, colorMix);
6415
+ baseColor = mix(baseColor, uColor3, uSpectralCentroid * uHigh * 2.5);
6416
+
6417
+ float energyLevel = uBeatEnergy * 0.35 + repulsion * uMid * 2.0 + uSpectralFlux * 0.8;
6418
+ vColor = mix(baseColor, vec3(1.0), energyLevel * 0.4);
6419
+ vColor += vec3(0.08, 0.04, 0.12) * uUltraHigh * 2.5;
6420
+
6421
+ vRimLight = pow(1.0 - max(0.0, dot(normalize(-mvPosition.xyz), normalize(newPos))), 3.0);
6422
+ vEnergy = energyLevel;
6423
+ vDepth = clamp(-mvPosition.z / 120.0, 0.0, 1.0);
6424
+
6425
6425
  float alpha = 1.0 - smoothstep(0.92, 1.05, max(abs(newPos.x) / (uViewportAspect * uViewportHalfHeight * 0.82), abs(newPos.y) / (uViewportHalfHeight * 0.82)));
6426
- float sparkle = 1.0 + (aRandom.x > 0.8 ? sin(uTime * 20.0 + aRandom.y * 150.0) * uHigh * 2.5 : 0.0);\r
6427
- vAlpha = alpha * sparkle * (0.7 + uMid * 0.35) * layerMod;\r
6428
- }\`;\r
6429
- \r
6430
- const fragmentShader = \`\r
6431
- varying vec3 vColor;\r
6426
+ float sparkle = 1.0 + (aRandom.x > 0.8 ? sin(uTime * 20.0 + aRandom.y * 150.0) * uHigh * 2.5 : 0.0);
6427
+ vAlpha = alpha * sparkle * (0.7 + uMid * 0.35) * layerMod;
6428
+ }\`;
6429
+
6430
+ const fragmentShader = \`
6431
+ varying vec3 vColor;
6432
6432
  varying float vAlpha, vEnergy, vDepth, vRimLight, vGlint;
6433
- \r
6434
- void main() {\r
6435
- vec2 xy = gl_PointCoord.xy - vec2(0.5);\r
6436
- float r = length(xy);\r
6437
- if(r > 0.5) discard;\r
6438
- \r
6439
- float core = smoothstep(0.16 - vEnergy * 0.04, 0.12 - vEnergy * 0.04, r);\r
6433
+
6434
+ void main() {
6435
+ vec2 xy = gl_PointCoord.xy - vec2(0.5);
6436
+ float r = length(xy);
6437
+ if(r > 0.5) discard;
6438
+
6439
+ float core = smoothstep(0.16 - vEnergy * 0.04, 0.12 - vEnergy * 0.04, r);
6440
6440
  float glow = exp(-r * r * (20.0 - vEnergy * 10.0));
6441
6441
  float halo = exp(-r * r * 5.0) * 0.35 + exp(-r * r * 2.5) * 0.15;
6442
6442
  float glint = vGlint * (1.0 - smoothstep(0.012, 0.045, min(abs(xy.x), abs(xy.y)))) * (1.0 - smoothstep(0.16, 0.48, r));
6443
6443
 
6444
6444
  vec3 finalColor = mix(vColor, vec3(1.0), core * 0.58 + glint * 0.38);
6445
6445
  finalColor += vColor * vRimLight * 0.1;
6446
- finalColor = mix(finalColor, vec3(0.02, 0.03, 0.05), vDepth * 0.4);\r
6447
- \r
6446
+ finalColor = mix(finalColor, vec3(0.02, 0.03, 0.05), vDepth * 0.4);
6447
+
6448
6448
  float finalAlpha = clamp(vAlpha * (glow * 0.85 + halo * 0.55 + pow(1.0 - r * 2.0, 2.5) * 0.55 + glint * 0.9), 0.0, 1.0);
6449
- gl_FragColor = vec4(finalColor, finalAlpha);\r
6450
- }\`;\r
6451
- \r
6452
- // \u2550\u2550\u2550\u2550\u2550\u2550\u2550 POST-PROCESSING SHADERS \u2550\u2550\u2550\u2550\u2550\u2550\u2550\r
6453
- \r
6454
- // GOD RAYS SHADER (Radial Light Scattering)\r
6455
- const GodRaysShader = {\r
6456
- uniforms: {\r
6457
- tDiffuse: { value: null },\r
6458
- uCenter: { value: new THREE.Vector2(0.5, 0.5) },\r
6459
- uIntensity: { value: 0.35 },\r
6460
- uDecay: { value: 0.94 },\r
6461
- uBeatEnergy: { value: 0.0 }\r
6462
- },\r
6463
- vertexShader: \`varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }\`,\r
6464
- fragmentShader: \`\r
6465
- uniform sampler2D tDiffuse;\r
6466
- uniform vec2 uCenter;\r
6467
- uniform float uIntensity, uDecay, uBeatEnergy;\r
6468
- varying vec2 vUv;\r
6469
- \r
6470
- void main() {\r
6471
- vec4 color = texture2D(tDiffuse, vUv);\r
6472
- vec2 dir = vUv - uCenter;\r
6473
- float dist = length(dir);\r
6474
- dir = normalize(dir);\r
6475
- \r
6476
- vec3 rays = vec3(0.0);\r
6477
- float decay = 1.0;\r
6478
- \r
6479
- for(int i = 0; i < 32; i++) {\r
6480
- float t = float(i) / 32.0;\r
6481
- vec2 samplePos = vUv - dir * t * 0.15 * (1.0 + uBeatEnergy);\r
6482
- vec4 s = texture2D(tDiffuse, samplePos);\r
6483
- float lum = dot(s.rgb, vec3(0.3, 0.59, 0.11));\r
6484
- rays += s.rgb * decay * step(0.35, lum);\r
6485
- decay *= uDecay;\r
6486
- }\r
6487
- rays /= 16.0;\r
6488
- rays *= (1.0 - smoothstep(0.3, 0.9, dist));\r
6489
- \r
6449
+ gl_FragColor = vec4(finalColor, finalAlpha);
6450
+ }\`;
6451
+
6452
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550 POST-PROCESSING SHADERS \u2550\u2550\u2550\u2550\u2550\u2550\u2550
6453
+
6454
+ // GOD RAYS SHADER (Radial Light Scattering)
6455
+ const GodRaysShader = {
6456
+ uniforms: {
6457
+ tDiffuse: { value: null },
6458
+ uCenter: { value: new THREE.Vector2(0.5, 0.5) },
6459
+ uIntensity: { value: 0.35 },
6460
+ uDecay: { value: 0.94 },
6461
+ uBeatEnergy: { value: 0.0 }
6462
+ },
6463
+ vertexShader: \`varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }\`,
6464
+ fragmentShader: \`
6465
+ uniform sampler2D tDiffuse;
6466
+ uniform vec2 uCenter;
6467
+ uniform float uIntensity, uDecay, uBeatEnergy;
6468
+ varying vec2 vUv;
6469
+
6470
+ void main() {
6471
+ vec4 color = texture2D(tDiffuse, vUv);
6472
+ vec2 dir = vUv - uCenter;
6473
+ float dist = length(dir);
6474
+ dir = normalize(dir);
6475
+
6476
+ vec3 rays = vec3(0.0);
6477
+ float decay = 1.0;
6478
+
6479
+ for(int i = 0; i < 32; i++) {
6480
+ float t = float(i) / 32.0;
6481
+ vec2 samplePos = vUv - dir * t * 0.15 * (1.0 + uBeatEnergy);
6482
+ vec4 s = texture2D(tDiffuse, samplePos);
6483
+ float lum = dot(s.rgb, vec3(0.3, 0.59, 0.11));
6484
+ rays += s.rgb * decay * step(0.35, lum);
6485
+ decay *= uDecay;
6486
+ }
6487
+ rays /= 16.0;
6488
+ rays *= (1.0 - smoothstep(0.3, 0.9, dist));
6489
+
6490
6490
  gl_FragColor = vec4(color.rgb + rays * uIntensity * (1.0 + uBeatEnergy * 0.8), color.a);
6491
- }\`\r
6492
- };\r
6493
- \r
6494
- // LENS FLARE POST-PROCESS SHADER (Cinematic)\r
6495
- const LensFlareShader = {\r
6496
- uniforms: {\r
6497
- tDiffuse: { value: null },\r
6498
- uIntensity: { value: 1.0 },\r
6499
- uBeatEnergy: { value: 0.0 },\r
6500
- uTime: { value: 0.0 }\r
6501
- },\r
6502
- vertexShader: \`varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }\`,\r
6503
- fragmentShader: \`\r
6504
- uniform sampler2D tDiffuse;\r
6505
- uniform float uIntensity, uBeatEnergy, uTime;\r
6506
- varying vec2 vUv;\r
6507
- \r
6508
- float sdHex(vec2 p) {\r
6509
- p = abs(p);\r
6510
- return max(p.x * 0.866 + p.y * 0.5, p.y) - 1.0;\r
6511
- }\r
6512
- \r
6513
- void main() {\r
6514
- vec4 color = texture2D(tDiffuse, vUv);\r
6515
- vec2 center = vec2(0.5);\r
6516
- vec2 toCenter = center - vUv;\r
6517
- float dist = length(toCenter);\r
6518
- vec2 dir = normalize(toCenter + 0.0001);\r
6519
- \r
6520
- vec3 flare = vec3(0.0);\r
6521
- \r
6522
- // Sample bright areas for flare source\r
6523
- float brightness = dot(color.rgb, vec3(0.299, 0.587, 0.114));\r
6524
- float threshold = smoothstep(0.6, 0.9, brightness);\r
6525
- \r
6526
- // Anamorphic horizontal streak\r
6527
- float streak = 0.0;\r
6528
- for(float i = -8.0; i <= 8.0; i += 1.0) {\r
6529
- vec2 streakUV = vUv + vec2(i * 0.012, 0.0);\r
6530
- vec4 streakSample = texture2D(tDiffuse, streakUV);\r
6531
- float streakBright = dot(streakSample.rgb, vec3(0.299, 0.587, 0.114));\r
6532
- float falloff = 1.0 - abs(i) / 10.0;\r
6533
- streak += smoothstep(0.7, 1.0, streakBright) * falloff * falloff;\r
6534
- }\r
6535
- flare += vec3(0.6, 0.7, 1.0) * streak * 0.08;\r
6536
- \r
6537
- // Subtle ghosts (reflections through lens elements)\r
6538
- for(int i = 1; i <= 3; i++) {\r
6539
- float scale = 1.0 - float(i) * 0.25;\r
6540
- vec2 ghostUV = center + (center - vUv) * scale * 0.8;\r
6541
- if(ghostUV.x > 0.0 && ghostUV.x < 1.0 && ghostUV.y > 0.0 && ghostUV.y < 1.0) {\r
6542
- vec4 ghostSample = texture2D(tDiffuse, ghostUV);\r
6543
- float ghostBright = dot(ghostSample.rgb, vec3(0.299, 0.587, 0.114));\r
6544
- float ghostAlpha = smoothstep(0.5, 0.8, ghostBright) * (0.25 / float(i));\r
6545
- vec3 ghostTint = vec3(0.7, 0.8, 1.0) * (1.0 - float(i) * 0.2);\r
6546
- flare += ghostSample.rgb * ghostTint * ghostAlpha;\r
6547
- }\r
6548
- }\r
6549
- \r
6550
- // Soft circular halo around bright center\r
6551
- float halo = smoothstep(0.25, 0.15, abs(dist - 0.2)) * threshold * 0.25;\r
6552
- flare += vec3(0.5, 0.6, 0.9) * halo;\r
6553
- \r
6554
- // Subtle vignette-edge glow\r
6555
- float edgeGlow = smoothstep(0.5, 0.7, dist) * (1.0 - smoothstep(0.7, 0.85, dist));\r
6556
- flare += vec3(0.3, 0.4, 0.6) * edgeGlow * brightness * 0.15;\r
6557
- \r
6491
+ }\`
6492
+ };
6493
+
6494
+ // LENS FLARE POST-PROCESS SHADER (Cinematic)
6495
+ const LensFlareShader = {
6496
+ uniforms: {
6497
+ tDiffuse: { value: null },
6498
+ uIntensity: { value: 1.0 },
6499
+ uBeatEnergy: { value: 0.0 },
6500
+ uTime: { value: 0.0 }
6501
+ },
6502
+ vertexShader: \`varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }\`,
6503
+ fragmentShader: \`
6504
+ uniform sampler2D tDiffuse;
6505
+ uniform float uIntensity, uBeatEnergy, uTime;
6506
+ varying vec2 vUv;
6507
+
6508
+ float sdHex(vec2 p) {
6509
+ p = abs(p);
6510
+ return max(p.x * 0.866 + p.y * 0.5, p.y) - 1.0;
6511
+ }
6512
+
6513
+ void main() {
6514
+ vec4 color = texture2D(tDiffuse, vUv);
6515
+ vec2 center = vec2(0.5);
6516
+ vec2 toCenter = center - vUv;
6517
+ float dist = length(toCenter);
6518
+ vec2 dir = normalize(toCenter + 0.0001);
6519
+
6520
+ vec3 flare = vec3(0.0);
6521
+
6522
+ // Sample bright areas for flare source
6523
+ float brightness = dot(color.rgb, vec3(0.299, 0.587, 0.114));
6524
+ float threshold = smoothstep(0.6, 0.9, brightness);
6525
+
6526
+ // Anamorphic horizontal streak
6527
+ float streak = 0.0;
6528
+ for(float i = -8.0; i <= 8.0; i += 1.0) {
6529
+ vec2 streakUV = vUv + vec2(i * 0.012, 0.0);
6530
+ vec4 streakSample = texture2D(tDiffuse, streakUV);
6531
+ float streakBright = dot(streakSample.rgb, vec3(0.299, 0.587, 0.114));
6532
+ float falloff = 1.0 - abs(i) / 10.0;
6533
+ streak += smoothstep(0.7, 1.0, streakBright) * falloff * falloff;
6534
+ }
6535
+ flare += vec3(0.6, 0.7, 1.0) * streak * 0.08;
6536
+
6537
+ // Subtle ghosts (reflections through lens elements)
6538
+ for(int i = 1; i <= 3; i++) {
6539
+ float scale = 1.0 - float(i) * 0.25;
6540
+ vec2 ghostUV = center + (center - vUv) * scale * 0.8;
6541
+ if(ghostUV.x > 0.0 && ghostUV.x < 1.0 && ghostUV.y > 0.0 && ghostUV.y < 1.0) {
6542
+ vec4 ghostSample = texture2D(tDiffuse, ghostUV);
6543
+ float ghostBright = dot(ghostSample.rgb, vec3(0.299, 0.587, 0.114));
6544
+ float ghostAlpha = smoothstep(0.5, 0.8, ghostBright) * (0.25 / float(i));
6545
+ vec3 ghostTint = vec3(0.7, 0.8, 1.0) * (1.0 - float(i) * 0.2);
6546
+ flare += ghostSample.rgb * ghostTint * ghostAlpha;
6547
+ }
6548
+ }
6549
+
6550
+ // Soft circular halo around bright center
6551
+ float halo = smoothstep(0.25, 0.15, abs(dist - 0.2)) * threshold * 0.25;
6552
+ flare += vec3(0.5, 0.6, 0.9) * halo;
6553
+
6554
+ // Subtle vignette-edge glow
6555
+ float edgeGlow = smoothstep(0.5, 0.7, dist) * (1.0 - smoothstep(0.7, 0.85, dist));
6556
+ flare += vec3(0.3, 0.4, 0.6) * edgeGlow * brightness * 0.15;
6557
+
6558
6558
  gl_FragColor = vec4(color.rgb + flare * uIntensity * (1.0 + uBeatEnergy * 0.3), color.a);
6559
- }\`\r
6560
- };\r
6561
- \r
6562
- // ANAMORPHIC FLARE SHADER\r
6563
- const AnamorphicShader = {\r
6564
- uniforms: {\r
6565
- tDiffuse: { value: null },\r
6566
- uStretch: { value: 0.1 },\r
6567
- uThreshold: { value: 0.7 },\r
6568
- uIntensity: { value: 0.4 }\r
6569
- },\r
6570
- vertexShader: \`varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }\`,\r
6571
- fragmentShader: \`\r
6572
- uniform sampler2D tDiffuse;\r
6573
- uniform float uStretch, uThreshold, uIntensity;\r
6574
- varying vec2 vUv;\r
6575
- void main() {\r
6576
- vec4 color = texture2D(tDiffuse, vUv);\r
6577
- vec3 streak = vec3(0.0);\r
6578
- for(float i = -15.0; i <= 15.0; i++) {\r
6579
- vec4 s = texture2D(tDiffuse, vUv + vec2(i * uStretch * 0.01, 0.0));\r
6580
- float bright = dot(s.rgb, vec3(0.299, 0.587, 0.114));\r
6581
- if(bright > uThreshold) {\r
6582
- float w = 1.0 - abs(i) / 15.0;\r
6583
- streak += s.rgb * w * w;\r
6584
- }\r
6585
- }\r
6586
- gl_FragColor = vec4(color.rgb + streak / 7.5 * vec3(0.6, 0.8, 1.0) * uIntensity, color.a);\r
6587
- }\`\r
6588
- };\r
6589
- \r
6590
- // CHROMATIC ABERRATION SHADER\r
6591
- const ChromaticShader = {\r
6592
- uniforms: {\r
6593
- tDiffuse: { value: null },\r
6594
- uIntensity: { value: 0.004 },\r
6595
- uBeatEnergy: { value: 0.0 }\r
6596
- },\r
6597
- vertexShader: \`varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }\`,\r
6598
- fragmentShader: \`\r
6599
- uniform sampler2D tDiffuse;\r
6600
- uniform float uIntensity, uBeatEnergy;\r
6601
- varying vec2 vUv;\r
6602
- void main() {\r
6603
- vec2 dir = vUv - vec2(0.5);\r
6604
- float dist = length(dir);\r
6605
- vec2 offset = dir * dist * uIntensity * (1.0 + uBeatEnergy * 1.5);\r
6606
- gl_FragColor = vec4(\r
6607
- texture2D(tDiffuse, vUv + offset * 1.2).r,\r
6608
- texture2D(tDiffuse, vUv).g,\r
6609
- texture2D(tDiffuse, vUv - offset * 1.2).b,\r
6559
+ }\`
6560
+ };
6561
+
6562
+ // ANAMORPHIC FLARE SHADER
6563
+ const AnamorphicShader = {
6564
+ uniforms: {
6565
+ tDiffuse: { value: null },
6566
+ uStretch: { value: 0.1 },
6567
+ uThreshold: { value: 0.7 },
6568
+ uIntensity: { value: 0.4 }
6569
+ },
6570
+ vertexShader: \`varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }\`,
6571
+ fragmentShader: \`
6572
+ uniform sampler2D tDiffuse;
6573
+ uniform float uStretch, uThreshold, uIntensity;
6574
+ varying vec2 vUv;
6575
+ void main() {
6576
+ vec4 color = texture2D(tDiffuse, vUv);
6577
+ vec3 streak = vec3(0.0);
6578
+ for(float i = -15.0; i <= 15.0; i++) {
6579
+ vec4 s = texture2D(tDiffuse, vUv + vec2(i * uStretch * 0.01, 0.0));
6580
+ float bright = dot(s.rgb, vec3(0.299, 0.587, 0.114));
6581
+ if(bright > uThreshold) {
6582
+ float w = 1.0 - abs(i) / 15.0;
6583
+ streak += s.rgb * w * w;
6584
+ }
6585
+ }
6586
+ gl_FragColor = vec4(color.rgb + streak / 7.5 * vec3(0.6, 0.8, 1.0) * uIntensity, color.a);
6587
+ }\`
6588
+ };
6589
+
6590
+ // CHROMATIC ABERRATION SHADER
6591
+ const ChromaticShader = {
6592
+ uniforms: {
6593
+ tDiffuse: { value: null },
6594
+ uIntensity: { value: 0.004 },
6595
+ uBeatEnergy: { value: 0.0 }
6596
+ },
6597
+ vertexShader: \`varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }\`,
6598
+ fragmentShader: \`
6599
+ uniform sampler2D tDiffuse;
6600
+ uniform float uIntensity, uBeatEnergy;
6601
+ varying vec2 vUv;
6602
+ void main() {
6603
+ vec2 dir = vUv - vec2(0.5);
6604
+ float dist = length(dir);
6605
+ vec2 offset = dir * dist * uIntensity * (1.0 + uBeatEnergy * 1.5);
6606
+ gl_FragColor = vec4(
6607
+ texture2D(tDiffuse, vUv + offset * 1.2).r,
6608
+ texture2D(tDiffuse, vUv).g,
6609
+ texture2D(tDiffuse, vUv - offset * 1.2).b,
6610
6610
  texture2D(tDiffuse, vUv).a
6611
- );\r
6612
- }\`\r
6613
- };\r
6614
- \r
6615
- // FILM GRAIN SHADER\r
6616
- const GrainShader = {\r
6617
- uniforms: {\r
6618
- tDiffuse: { value: null },\r
6619
- uTime: { value: 0.0 },\r
6620
- uIntensity: { value: 0.0 }\r
6621
- },\r
6622
- vertexShader: \`varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }\`,\r
6623
- fragmentShader: \`\r
6624
- uniform sampler2D tDiffuse;\r
6625
- uniform float uTime, uIntensity;\r
6626
- varying vec2 vUv;\r
6627
- float rand(vec2 co) { return fract(sin(dot(co, vec2(12.9898,78.233))) * 43758.5453); }\r
6628
- void main() {\r
6629
- vec4 color = texture2D(tDiffuse, vUv);\r
6630
- float grain = (rand(vUv + fract(uTime)) * 2.0 - 1.0) * uIntensity;\r
6631
- gl_FragColor = vec4(color.rgb + grain * (1.0 - dot(color.rgb, vec3(0.299, 0.587, 0.114)) * 0.5), color.a);\r
6632
- }\`\r
6633
- };\r
6634
- \r
6635
- // CUSTOM DEPTH OF FIELD SHADER (Radial + Edge blur)\r
6636
- const CustomDOFShader = {\r
6637
- uniforms: {\r
6638
- tDiffuse: { value: null },\r
6639
- uFocus: { value: 0.05 },\r
6640
- uFocalLength: { value: 0.25 },\r
6641
- uBokehStrength: { value: 0.5 },\r
6642
- uResolution: { value: new THREE.Vector2() },\r
6643
- uBeatEnergy: { value: 0.0 }\r
6644
- },\r
6645
- vertexShader: \`\r
6646
- varying vec2 vUv;\r
6647
- void main() {\r
6648
- vUv = uv;\r
6649
- gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\r
6650
- }\r
6651
- \`,\r
6652
- fragmentShader: \`\r
6653
- uniform sampler2D tDiffuse;\r
6654
- uniform float uFocus;\r
6655
- uniform float uFocalLength;\r
6656
- uniform float uBokehStrength;\r
6657
- uniform vec2 uResolution;\r
6658
- uniform float uBeatEnergy;\r
6659
- varying vec2 vUv;\r
6660
- \r
6661
- void main() {\r
6662
- vec2 center = vec2(0.5);\r
6663
- float dist = length(vUv - center);\r
6664
- \r
6665
- // Focus ring - sharp in a ring around center, blurry inside and outside\r
6666
- float focusRing = abs(dist - uFocus);\r
6667
- float blur = smoothstep(0.0, uFocalLength, focusRing) * uBokehStrength;\r
6668
- \r
6669
- vec3 color = vec3(0.0);\r
6670
- float total = 0.0;\r
6671
- vec2 texel = 1.0 / uResolution;\r
6672
- \r
6673
- if(blur < 0.01) {\r
6674
- color = texture2D(tDiffuse, vUv).rgb;\r
6675
- total = 1.0;\r
6676
- } else {\r
6677
- // Golden angle spiral sampling\r
6678
- const float GOLDEN_ANGLE = 2.39996323;\r
6679
- const int SAMPLES = 24;\r
6680
- float radius = blur * 20.0 * (1.0 + uBeatEnergy * 0.1);\r
6681
- \r
6682
- for(int i = 0; i < SAMPLES; i++) {\r
6683
- float angle = float(i) * GOLDEN_ANGLE;\r
6684
- float r = sqrt(float(i) / float(SAMPLES)) * radius;\r
6685
- vec2 offset = vec2(cos(angle), sin(angle)) * r * texel;\r
6686
- vec3 s = texture2D(tDiffuse, vUv + offset).rgb;\r
6687
- \r
6688
- // Subtle highlight boost\r
6689
- float lum = dot(s, vec3(0.299, 0.587, 0.114));\r
6690
- s *= 1.0 + smoothstep(0.7, 1.0, lum) * blur * 0.3;\r
6691
- \r
6692
- color += s;\r
6693
- total += 1.0;\r
6694
- }\r
6695
- }\r
6696
- \r
6611
+ );
6612
+ }\`
6613
+ };
6614
+
6615
+ // FILM GRAIN SHADER
6616
+ const GrainShader = {
6617
+ uniforms: {
6618
+ tDiffuse: { value: null },
6619
+ uTime: { value: 0.0 },
6620
+ uIntensity: { value: 0.0 }
6621
+ },
6622
+ vertexShader: \`varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }\`,
6623
+ fragmentShader: \`
6624
+ uniform sampler2D tDiffuse;
6625
+ uniform float uTime, uIntensity;
6626
+ varying vec2 vUv;
6627
+ float rand(vec2 co) { return fract(sin(dot(co, vec2(12.9898,78.233))) * 43758.5453); }
6628
+ void main() {
6629
+ vec4 color = texture2D(tDiffuse, vUv);
6630
+ float grain = (rand(vUv + fract(uTime)) * 2.0 - 1.0) * uIntensity;
6631
+ gl_FragColor = vec4(color.rgb + grain * (1.0 - dot(color.rgb, vec3(0.299, 0.587, 0.114)) * 0.5), color.a);
6632
+ }\`
6633
+ };
6634
+
6635
+ // CUSTOM DEPTH OF FIELD SHADER (Radial + Edge blur)
6636
+ const CustomDOFShader = {
6637
+ uniforms: {
6638
+ tDiffuse: { value: null },
6639
+ uFocus: { value: 0.05 },
6640
+ uFocalLength: { value: 0.25 },
6641
+ uBokehStrength: { value: 0.5 },
6642
+ uResolution: { value: new THREE.Vector2() },
6643
+ uBeatEnergy: { value: 0.0 }
6644
+ },
6645
+ vertexShader: \`
6646
+ varying vec2 vUv;
6647
+ void main() {
6648
+ vUv = uv;
6649
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
6650
+ }
6651
+ \`,
6652
+ fragmentShader: \`
6653
+ uniform sampler2D tDiffuse;
6654
+ uniform float uFocus;
6655
+ uniform float uFocalLength;
6656
+ uniform float uBokehStrength;
6657
+ uniform vec2 uResolution;
6658
+ uniform float uBeatEnergy;
6659
+ varying vec2 vUv;
6660
+
6661
+ void main() {
6662
+ vec2 center = vec2(0.5);
6663
+ float dist = length(vUv - center);
6664
+
6665
+ // Focus ring - sharp in a ring around center, blurry inside and outside
6666
+ float focusRing = abs(dist - uFocus);
6667
+ float blur = smoothstep(0.0, uFocalLength, focusRing) * uBokehStrength;
6668
+
6669
+ vec3 color = vec3(0.0);
6670
+ float total = 0.0;
6671
+ vec2 texel = 1.0 / uResolution;
6672
+
6673
+ if(blur < 0.01) {
6674
+ color = texture2D(tDiffuse, vUv).rgb;
6675
+ total = 1.0;
6676
+ } else {
6677
+ // Golden angle spiral sampling
6678
+ const float GOLDEN_ANGLE = 2.39996323;
6679
+ const int SAMPLES = 24;
6680
+ float radius = blur * 20.0 * (1.0 + uBeatEnergy * 0.1);
6681
+
6682
+ for(int i = 0; i < SAMPLES; i++) {
6683
+ float angle = float(i) * GOLDEN_ANGLE;
6684
+ float r = sqrt(float(i) / float(SAMPLES)) * radius;
6685
+ vec2 offset = vec2(cos(angle), sin(angle)) * r * texel;
6686
+ vec3 s = texture2D(tDiffuse, vUv + offset).rgb;
6687
+
6688
+ // Subtle highlight boost
6689
+ float lum = dot(s, vec3(0.299, 0.587, 0.114));
6690
+ s *= 1.0 + smoothstep(0.7, 1.0, lum) * blur * 0.3;
6691
+
6692
+ color += s;
6693
+ total += 1.0;
6694
+ }
6695
+ }
6696
+
6697
6697
  gl_FragColor = vec4(color / total, texture2D(tDiffuse, vUv).a);
6698
- }\r
6699
- \`\r
6700
- };\r
6701
- \r
6702
- // VIGNETTE + GLOW SHADER\r
6703
- const VignetteShader = {\r
6704
- uniforms: {\r
6705
- tDiffuse: { value: null },\r
6706
- uBeatEnergy: { value: 0.0 }\r
6707
- },\r
6708
- vertexShader: \`varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }\`,\r
6709
- fragmentShader: \`\r
6710
- uniform sampler2D tDiffuse;\r
6711
- uniform float uBeatEnergy;\r
6712
- varying vec2 vUv;\r
6713
- void main() {\r
6714
- vec4 color = texture2D(tDiffuse, vUv);\r
6715
- vec2 uv = vUv * (1.0 - vUv.yx);\r
6716
- float vig = pow(uv.x * uv.y * 15.0, 0.38 * (1.0 - uBeatEnergy * 0.15));\r
6717
- float lum = dot(color.rgb, vec3(0.299, 0.587, 0.114));\r
6718
- float glow = smoothstep(0.5, 0.9, lum) * 0.1 * (1.0 + uBeatEnergy * 0.5);\r
6719
- gl_FragColor = vec4(color.rgb * vig * (1.0 + glow), color.a);\r
6720
- }\`\r
6721
- };\r
6722
- \r
6723
- // \u2550\u2550\u2550\u2550\u2550\u2550\u2550 GEOMETRY \u2550\u2550\u2550\u2550\u2550\u2550\u2550\r
6724
- const geometry = new THREE.BufferGeometry();\r
6698
+ }
6699
+ \`
6700
+ };
6701
+
6702
+ // VIGNETTE + GLOW SHADER
6703
+ const VignetteShader = {
6704
+ uniforms: {
6705
+ tDiffuse: { value: null },
6706
+ uBeatEnergy: { value: 0.0 }
6707
+ },
6708
+ vertexShader: \`varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }\`,
6709
+ fragmentShader: \`
6710
+ uniform sampler2D tDiffuse;
6711
+ uniform float uBeatEnergy;
6712
+ varying vec2 vUv;
6713
+ void main() {
6714
+ vec4 color = texture2D(tDiffuse, vUv);
6715
+ vec2 uv = vUv * (1.0 - vUv.yx);
6716
+ float vig = pow(uv.x * uv.y * 15.0, 0.38 * (1.0 - uBeatEnergy * 0.15));
6717
+ float lum = dot(color.rgb, vec3(0.299, 0.587, 0.114));
6718
+ float glow = smoothstep(0.5, 0.9, lum) * 0.1 * (1.0 + uBeatEnergy * 0.5);
6719
+ gl_FragColor = vec4(color.rgb * vig * (1.0 + glow), color.a);
6720
+ }\`
6721
+ };
6722
+
6723
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550 GEOMETRY \u2550\u2550\u2550\u2550\u2550\u2550\u2550
6724
+ const geometry = new THREE.BufferGeometry();
6725
6725
  const mainCapacity = Math.ceil(CONFIG.maxParticles * CONFIG.maxDensity);
6726
6726
  const pos = new Float32Array(mainCapacity * 3);
6727
6727
  const rnd = new Float32Array(mainCapacity * 3);
@@ -6730,11 +6730,11 @@ void main() {\r
6730
6730
  const vec = new THREE.Vector3();
6731
6731
 
6732
6732
  for (let i = 0; i < mainCapacity; i++) {
6733
- vec.setFromSphericalCoords(\r
6734
- CONFIG.fieldRadius * Math.pow(Math.random(), 0.33),\r
6735
- Math.acos(2 * Math.random() - 1),\r
6736
- Math.random() * Math.PI * 2\r
6737
- );\r
6733
+ vec.setFromSphericalCoords(
6734
+ CONFIG.fieldRadius * Math.pow(Math.random(), 0.33),
6735
+ Math.acos(2 * Math.random() - 1),
6736
+ Math.random() * Math.PI * 2
6737
+ );
6738
6738
  pos[i * 3] = vec.x;
6739
6739
  pos[i * 3 + 1] = vec.y;
6740
6740
  pos[i * 3 + 2] = vec.z;
@@ -6742,57 +6742,57 @@ void main() {\r
6742
6742
  rnd[i * 3 + 1] = Math.random();
6743
6743
  rnd[i * 3 + 2] = Math.random();
6744
6744
  phase[i] = Math.random() * Math.PI * 2;
6745
- }\r
6746
- \r
6747
- // Secondary particles now use separate instanced system below\r
6748
- \r
6745
+ }
6746
+
6747
+ // Secondary particles now use separate instanced system below
6748
+
6749
6749
  geometry.setAttribute('position', new THREE.BufferAttribute(pos, 3));
6750
6750
  geometry.setAttribute('aRandom', new THREE.BufferAttribute(rnd, 3));
6751
6751
  geometry.setAttribute('aPhase', new THREE.BufferAttribute(phase, 1));
6752
6752
  geometry.setAttribute('aLayer', new THREE.BufferAttribute(layer, 1));
6753
- geometry.setDrawRange(0, Math.floor(CONFIG.maxParticles * STATE.density));\r
6754
- \r
6755
- const FIELD_TYPES = {\r
6753
+ geometry.setDrawRange(0, Math.floor(CONFIG.maxParticles * STATE.density));
6754
+
6755
+ const FIELD_TYPES = {
6756
6756
  'Photon (EM)': { c1: '#00f2ff', c2: '#0051ff', c3: '#ffffff', s: 1.0, curl: 1.0, sz: 3.0, sc1: '#66d9ff', sc2: '#99eeff' },
6757
6757
  'Gluon (Strong)': { c1: '#ff0055', c2: '#7000ff', c3: '#ff8800', s: 2.5, curl: 2.4, sz: 3.4, sc1: '#cc66ff', sc2: '#ff6699' },
6758
6758
  'Higgs (Mass)': { c1: '#ffae00', c2: '#ff4800', c3: '#ffff00', s: 0.6, curl: 0.3, sz: 4.0, sc1: '#ffcc66', sc2: '#ffdd99' },
6759
6759
  'Gravity': { c1: '#ffffff', c2: '#4466ff', c3: '#8844ff', s: 0.75, curl: 0.18, sz: 2.8, sc1: '#aabbff', sc2: '#ccaaff' },
6760
6760
  'Dark Energy': { c1: '#1a0033', c2: '#6600ff', c3: '#ff00ff', s: 1.9, curl: 1.6, sz: 3.1, sc1: '#9933ff', sc2: '#cc66ff' },
6761
6761
  'Neutrino': { c1: '#00ff88', c2: '#00ffcc', c3: '#88ffff', s: 3.0, curl: 3.0, sz: 2.6, sc1: '#66ffbb', sc2: '#99ffdd' },
6762
- // === ENHANCEMENT: Waveform Terrain Field ===\r
6762
+ // === ENHANCEMENT: Waveform Terrain Field ===
6763
6763
  'Waveform Terrain': { c1: '#ff3366', c2: '#33ff99', c3: '#6633ff', s: 0.4, curl: 0.15, sz: 2.4, sc1: '#ff6699', sc2: '#66ffcc', terrain: true }
6764
- };\r
6765
- \r
6766
- // === ENHANCEMENT: Adaptive Quality System ===\r
6767
- const QUALITY_SYSTEM = {\r
6768
- frameCount: 0,\r
6769
- frameTimes: [],\r
6770
- lastAdjustTime: 0,\r
6771
- currentFPS: 60,\r
6772
- \r
6773
- update(deltaTime) {\r
6774
- this.frameCount++;\r
6775
- this.frameTimes.push(deltaTime);\r
6776
- if (this.frameTimes.length > 30) this.frameTimes.shift();\r
6777
- \r
6778
- if (this.frameCount % 30 === 0) {\r
6779
- const avgDelta = this.frameTimes.reduce((a, b) => a + b, 0) / this.frameTimes.length;\r
6780
- this.currentFPS = 1000 / avgDelta;\r
6781
- \r
6764
+ };
6765
+
6766
+ // === ENHANCEMENT: Adaptive Quality System ===
6767
+ const QUALITY_SYSTEM = {
6768
+ frameCount: 0,
6769
+ frameTimes: [],
6770
+ lastAdjustTime: 0,
6771
+ currentFPS: 60,
6772
+
6773
+ update(deltaTime) {
6774
+ this.frameCount++;
6775
+ this.frameTimes.push(deltaTime);
6776
+ if (this.frameTimes.length > 30) this.frameTimes.shift();
6777
+
6778
+ if (this.frameCount % 30 === 0) {
6779
+ const avgDelta = this.frameTimes.reduce((a, b) => a + b, 0) / this.frameTimes.length;
6780
+ this.currentFPS = 1000 / avgDelta;
6781
+
6782
6782
  if (STATE.adaptiveQualityEnabled && performance.now() - this.lastAdjustTime > 1000) {
6783
6783
  if (this.currentFPS < STATE.targetFPS - 10 && STATE.qualityLevel > 0.3) {
6784
6784
  STATE.qualityLevel = Math.max(0.3, STATE.qualityLevel - (this.currentFPS < STATE.targetFPS * 0.6 ? 0.2 : 0.1));
6785
- this.applyQuality();\r
6786
- this.lastAdjustTime = performance.now();\r
6787
- } else if (this.currentFPS > STATE.targetFPS + 5 && STATE.qualityLevel < 1.0) {\r
6788
- STATE.qualityLevel = Math.min(1.0, STATE.qualityLevel + 0.05);\r
6789
- this.applyQuality();\r
6790
- this.lastAdjustTime = performance.now();\r
6791
- }\r
6792
- }\r
6793
- }\r
6794
- },\r
6795
- \r
6785
+ this.applyQuality();
6786
+ this.lastAdjustTime = performance.now();
6787
+ } else if (this.currentFPS > STATE.targetFPS + 5 && STATE.qualityLevel < 1.0) {
6788
+ STATE.qualityLevel = Math.min(1.0, STATE.qualityLevel + 0.05);
6789
+ this.applyQuality();
6790
+ this.lastAdjustTime = performance.now();
6791
+ }
6792
+ }
6793
+ }
6794
+ },
6795
+
6796
6796
  applyQuality() {
6797
6797
  const q = STATE.qualityLevel;
6798
6798
  const pixelRatio = Math.min(STATE.pixelRatio, Math.max(0.35, STATE.pixelRatio * q));
@@ -6809,301 +6809,301 @@ void main() {\r
6809
6809
  material.uniforms.uPixelRatio.value = pixelRatio;
6810
6810
  secondaryMaterial.uniforms.uPixelRatio.value = pixelRatio;
6811
6811
  }
6812
- };\r
6813
- \r
6814
- // === ENHANCEMENT: Audio-Reactive Camera System ===\r
6815
- const AUDIO_CAMERA = {\r
6816
- targetOffset: new THREE.Vector3(),\r
6817
- currentOffset: new THREE.Vector3(),\r
6818
- targetFOV: 55,\r
6819
- baseFOV: 55,\r
6820
- breathPhase: 0,\r
6821
- \r
6822
- update(deltaTime) {\r
6823
- if (!STATE.audioCameraEnabled || !AUDIO.active) return;\r
6824
- \r
6825
- const bass = AUDIO.gatedBands.bass;\r
6826
- const mid = AUDIO.gatedBands.mid;\r
6827
- const high = AUDIO.gatedBands.high;\r
6828
- const intensity = STATE.audioCameraIntensity;\r
6829
- const smooth = STATE.audioCameraSmoothing;\r
6830
- \r
6831
- // Bass-driven zoom pulse\r
6832
- this.targetFOV = this.baseFOV - bass * 8 * intensity + AUDIO.beatEnergy * 5 * intensity;\r
6833
- camera.fov += (this.targetFOV - camera.fov) * smooth;\r
6834
- camera.updateProjectionMatrix();\r
6835
- \r
6836
- // Subtle sway based on mid frequencies\r
6837
- this.breathPhase += deltaTime * 0.001 * (1 + mid * 2);\r
6838
- this.targetOffset.x = Math.sin(this.breathPhase * 0.7) * mid * 3 * intensity;\r
6839
- this.targetOffset.y = Math.cos(this.breathPhase * 0.5) * high * 2 * intensity;\r
6840
- this.targetOffset.z = Math.sin(this.breathPhase * 0.3) * bass * 4 * intensity;\r
6841
- \r
6842
- // Smooth interpolation\r
6843
- this.currentOffset.lerp(this.targetOffset, smooth);\r
6844
- \r
6845
- // Apply onset punch\r
6846
- if (AUDIO.onsetDetected) {\r
6847
- this.currentOffset.z -= AUDIO.onsetEnergy * 2 * intensity;\r
6848
- }\r
6849
- }\r
6850
- };\r
6851
- \r
6852
- const def = FIELD_TYPES[STATE.field];\r
6853
- const material = new THREE.ShaderMaterial({\r
6854
- vertexShader, fragmentShader,\r
6855
- uniforms: {\r
6856
- uTime: { value: 0 },\r
6857
- uPixelRatio: { value: STATE.pixelRatio },\r
6858
- uSizeBase: { value: def.sz },\r
6859
- uNoiseScale: { value: def.s },\r
6860
- uCurlStrength: { value: def.curl },\r
6812
+ };
6813
+
6814
+ // === ENHANCEMENT: Audio-Reactive Camera System ===
6815
+ const AUDIO_CAMERA = {
6816
+ targetOffset: new THREE.Vector3(),
6817
+ currentOffset: new THREE.Vector3(),
6818
+ targetFOV: 55,
6819
+ baseFOV: 55,
6820
+ breathPhase: 0,
6821
+
6822
+ update(deltaTime) {
6823
+ if (!STATE.audioCameraEnabled || !AUDIO.active) return;
6824
+
6825
+ const bass = AUDIO.gatedBands.bass;
6826
+ const mid = AUDIO.gatedBands.mid;
6827
+ const high = AUDIO.gatedBands.high;
6828
+ const intensity = STATE.audioCameraIntensity;
6829
+ const smooth = STATE.audioCameraSmoothing;
6830
+
6831
+ // Bass-driven zoom pulse
6832
+ this.targetFOV = this.baseFOV - bass * 8 * intensity + AUDIO.beatEnergy * 5 * intensity;
6833
+ camera.fov += (this.targetFOV - camera.fov) * smooth;
6834
+ camera.updateProjectionMatrix();
6835
+
6836
+ // Subtle sway based on mid frequencies
6837
+ this.breathPhase += deltaTime * 0.001 * (1 + mid * 2);
6838
+ this.targetOffset.x = Math.sin(this.breathPhase * 0.7) * mid * 3 * intensity;
6839
+ this.targetOffset.y = Math.cos(this.breathPhase * 0.5) * high * 2 * intensity;
6840
+ this.targetOffset.z = Math.sin(this.breathPhase * 0.3) * bass * 4 * intensity;
6841
+
6842
+ // Smooth interpolation
6843
+ this.currentOffset.lerp(this.targetOffset, smooth);
6844
+
6845
+ // Apply onset punch
6846
+ if (AUDIO.onsetDetected) {
6847
+ this.currentOffset.z -= AUDIO.onsetEnergy * 2 * intensity;
6848
+ }
6849
+ }
6850
+ };
6851
+
6852
+ const def = FIELD_TYPES[STATE.field];
6853
+ const material = new THREE.ShaderMaterial({
6854
+ vertexShader, fragmentShader,
6855
+ uniforms: {
6856
+ uTime: { value: 0 },
6857
+ uPixelRatio: { value: STATE.pixelRatio },
6858
+ uSizeBase: { value: def.sz },
6859
+ uNoiseScale: { value: def.s },
6860
+ uCurlStrength: { value: def.curl },
6861
6861
  uViewportAspect: { value: window.innerWidth / window.innerHeight },
6862
6862
  uViewportHalfHeight: { value: Math.tan(camera.fov * Math.PI / 360) * CONFIG.defaultZoom },
6863
- uColor1: { value: new THREE.Color(def.c1) },\r
6864
- uColor2: { value: new THREE.Color(def.c2) },\r
6865
- uColor3: { value: new THREE.Color(def.c3) },\r
6866
- uSubBass: { value: 0 }, uBass: { value: 0 }, uLowMid: { value: 0 },\r
6867
- uMid: { value: 0 }, uHighMid: { value: 0 }, uHigh: { value: 0 }, uUltraHigh: { value: 0 },\r
6868
- uBeatEnergy: { value: 0 }, uSpectralCentroid: { value: 0 }, uSpectralFlux: { value: 0 },\r
6869
- uOnsetEnergy: { value: 0 }, uTerrainMode: { value: 0 }, uTerrainHeight: { value: 15.0 },\r
6870
- uVortexStrength: { value: STATE.vortexStrength },\r
6871
- uPulseIntensity: { value: STATE.pulseIntensity },\r
6872
- uZoomFactor: { value: 1.0 },\r
6873
- uMousePos: { value: new THREE.Vector3(999, 999, 999) },\r
6874
- uMouseVelocity: { value: new THREE.Vector2(0, 0) }\r
6875
- },\r
6876
- transparent: true, depthWrite: false, blending: THREE.AdditiveBlending\r
6877
- });\r
6878
- \r
6879
- const points = new THREE.Points(geometry, material);\r
6880
- scene.add(points);\r
6881
- \r
6882
- // \u2550\u2550\u2550\u2550\u2550\u2550\u2550 INSTANCED SECONDARY PARTICLES \u2550\u2550\u2550\u2550\u2550\u2550\u2550\r
6863
+ uColor1: { value: new THREE.Color(def.c1) },
6864
+ uColor2: { value: new THREE.Color(def.c2) },
6865
+ uColor3: { value: new THREE.Color(def.c3) },
6866
+ uSubBass: { value: 0 }, uBass: { value: 0 }, uLowMid: { value: 0 },
6867
+ uMid: { value: 0 }, uHighMid: { value: 0 }, uHigh: { value: 0 }, uUltraHigh: { value: 0 },
6868
+ uBeatEnergy: { value: 0 }, uSpectralCentroid: { value: 0 }, uSpectralFlux: { value: 0 },
6869
+ uOnsetEnergy: { value: 0 }, uTerrainMode: { value: 0 }, uTerrainHeight: { value: 15.0 },
6870
+ uVortexStrength: { value: STATE.vortexStrength },
6871
+ uPulseIntensity: { value: STATE.pulseIntensity },
6872
+ uZoomFactor: { value: 1.0 },
6873
+ uMousePos: { value: new THREE.Vector3(999, 999, 999) },
6874
+ uMouseVelocity: { value: new THREE.Vector2(0, 0) }
6875
+ },
6876
+ transparent: true, depthWrite: false, blending: THREE.AdditiveBlending
6877
+ });
6878
+
6879
+ const points = new THREE.Points(geometry, material);
6880
+ scene.add(points);
6881
+
6882
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550 INSTANCED SECONDARY PARTICLES \u2550\u2550\u2550\u2550\u2550\u2550\u2550
6883
6883
  const secondaryVertexShader = \`
6884
6884
  uniform float uTime, uPixelRatio;
6885
6885
  uniform float uViewportAspect, uViewportHalfHeight;
6886
- uniform float uSubBass, uBass, uLowMid, uMid, uHighMid, uHigh, uUltraHigh;\r
6887
- uniform float uBeatEnergy, uSpectralCentroid, uSpectralFlux, uOnsetEnergy;\r
6888
- uniform vec3 uColor1, uColor2;\r
6886
+ uniform float uSubBass, uBass, uLowMid, uMid, uHighMid, uHigh, uUltraHigh;
6887
+ uniform float uBeatEnergy, uSpectralCentroid, uSpectralFlux, uOnsetEnergy;
6888
+ uniform vec3 uColor1, uColor2;
6889
6889
  uniform float uVortexStrength;
6890
- \r
6891
- attribute vec3 instancePosition;\r
6892
- attribute vec3 instanceRandom;\r
6893
- attribute float instancePhase;\r
6894
- \r
6895
- varying vec3 vColor;\r
6896
- varying float vAlpha;\r
6897
- varying float vEnergy;\r
6898
- varying float vDepth;\r
6899
- \r
6900
- vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }\r
6901
- vec4 mod289(vec4 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }\r
6902
- vec4 permute(vec4 x) { return mod289(((x*34.0)+1.0)*x); }\r
6903
- vec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; }\r
6904
- \r
6905
- float snoise(vec3 v) {\r
6906
- const vec2 C = vec2(1.0/6.0, 1.0/3.0);\r
6907
- const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);\r
6908
- vec3 i = floor(v + dot(v, C.yyy));\r
6909
- vec3 x0 = v - i + dot(i, C.xxx);\r
6910
- vec3 g = step(x0.yzx, x0.xyz);\r
6911
- vec3 l = 1.0 - g;\r
6912
- vec3 i1 = min(g.xyz, l.zxy);\r
6913
- vec3 i2 = max(g.xyz, l.zxy);\r
6914
- vec3 x1 = x0 - i1 + C.xxx;\r
6915
- vec3 x2 = x0 - i2 + C.yyy;\r
6916
- vec3 x3 = x0 - D.yyy;\r
6917
- i = mod289(i);\r
6918
- vec4 p = permute(permute(permute(i.z + vec4(0.0, i1.z, i2.z, 1.0)) + i.y + vec4(0.0, i1.y, i2.y, 1.0)) + i.x + vec4(0.0, i1.x, i2.x, 1.0));\r
6919
- float n_ = 0.142857142857;\r
6920
- vec3 ns = n_ * D.wyz - D.xzx;\r
6921
- vec4 j = p - 49.0 * floor(p * ns.z * ns.z);\r
6922
- vec4 x_ = floor(j * ns.z);\r
6923
- vec4 y_ = floor(j - 7.0 * x_);\r
6924
- vec4 x = x_ * ns.x + ns.yyyy;\r
6925
- vec4 y = y_ * ns.x + ns.yyyy;\r
6926
- vec4 h = 1.0 - abs(x) - abs(y);\r
6927
- vec4 b0 = vec4(x.xy, y.xy);\r
6928
- vec4 b1 = vec4(x.zw, y.zw);\r
6929
- vec4 s0 = floor(b0) * 2.0 + 1.0;\r
6930
- vec4 s1 = floor(b1) * 2.0 + 1.0;\r
6931
- vec4 sh = -step(h, vec4(0.0));\r
6932
- vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy;\r
6933
- vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww;\r
6934
- vec3 p0 = vec3(a0.xy, h.x);\r
6935
- vec3 p1 = vec3(a0.zw, h.y);\r
6936
- vec3 p2 = vec3(a1.xy, h.z);\r
6937
- vec3 p3 = vec3(a1.zw, h.w);\r
6938
- vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2,p2), dot(p3,p3)));\r
6939
- p0 *= norm.x; p1 *= norm.y; p2 *= norm.z; p3 *= norm.w;\r
6940
- vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);\r
6941
- m = m * m;\r
6942
- return 42.0 * dot(m*m, vec4(dot(p0,x0), dot(p1,x1), dot(p2,x2), dot(p3,x3)));\r
6943
- }\r
6944
- \r
6890
+
6891
+ attribute vec3 instancePosition;
6892
+ attribute vec3 instanceRandom;
6893
+ attribute float instancePhase;
6894
+
6895
+ varying vec3 vColor;
6896
+ varying float vAlpha;
6897
+ varying float vEnergy;
6898
+ varying float vDepth;
6899
+
6900
+ vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
6901
+ vec4 mod289(vec4 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
6902
+ vec4 permute(vec4 x) { return mod289(((x*34.0)+1.0)*x); }
6903
+ vec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; }
6904
+
6905
+ float snoise(vec3 v) {
6906
+ const vec2 C = vec2(1.0/6.0, 1.0/3.0);
6907
+ const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
6908
+ vec3 i = floor(v + dot(v, C.yyy));
6909
+ vec3 x0 = v - i + dot(i, C.xxx);
6910
+ vec3 g = step(x0.yzx, x0.xyz);
6911
+ vec3 l = 1.0 - g;
6912
+ vec3 i1 = min(g.xyz, l.zxy);
6913
+ vec3 i2 = max(g.xyz, l.zxy);
6914
+ vec3 x1 = x0 - i1 + C.xxx;
6915
+ vec3 x2 = x0 - i2 + C.yyy;
6916
+ vec3 x3 = x0 - D.yyy;
6917
+ i = mod289(i);
6918
+ vec4 p = permute(permute(permute(i.z + vec4(0.0, i1.z, i2.z, 1.0)) + i.y + vec4(0.0, i1.y, i2.y, 1.0)) + i.x + vec4(0.0, i1.x, i2.x, 1.0));
6919
+ float n_ = 0.142857142857;
6920
+ vec3 ns = n_ * D.wyz - D.xzx;
6921
+ vec4 j = p - 49.0 * floor(p * ns.z * ns.z);
6922
+ vec4 x_ = floor(j * ns.z);
6923
+ vec4 y_ = floor(j - 7.0 * x_);
6924
+ vec4 x = x_ * ns.x + ns.yyyy;
6925
+ vec4 y = y_ * ns.x + ns.yyyy;
6926
+ vec4 h = 1.0 - abs(x) - abs(y);
6927
+ vec4 b0 = vec4(x.xy, y.xy);
6928
+ vec4 b1 = vec4(x.zw, y.zw);
6929
+ vec4 s0 = floor(b0) * 2.0 + 1.0;
6930
+ vec4 s1 = floor(b1) * 2.0 + 1.0;
6931
+ vec4 sh = -step(h, vec4(0.0));
6932
+ vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy;
6933
+ vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww;
6934
+ vec3 p0 = vec3(a0.xy, h.x);
6935
+ vec3 p1 = vec3(a0.zw, h.y);
6936
+ vec3 p2 = vec3(a1.xy, h.z);
6937
+ vec3 p3 = vec3(a1.zw, h.w);
6938
+ vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2,p2), dot(p3,p3)));
6939
+ p0 *= norm.x; p1 *= norm.y; p2 *= norm.z; p3 *= norm.w;
6940
+ vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);
6941
+ m = m * m;
6942
+ return 42.0 * dot(m*m, vec4(dot(p0,x0), dot(p1,x1), dot(p2,x2), dot(p3,x3)));
6943
+ }
6944
+
6945
6945
  \${shapeFunction}
6946
6946
 
6947
6947
  void main() {
6948
6948
  // Secondary particles run at a slower timescale \u2014 they are the atmosphere, not the core
6949
- float t = uTime * 0.45;\r
6950
- \r
6951
- // Multi-scale noise flow \u2014 low freqs drive large drift, high freqs add fine turbulence\r
6952
- vec3 noisePos = instancePosition * 0.035 + instanceRandom;\r
6953
- vec3 drift = vec3(\r
6954
- snoise(noisePos + t * 0.06),\r
6955
- snoise(noisePos * 0.9 - t * 0.05),\r
6956
- snoise(noisePos * 1.1 + t * 0.04)\r
6957
- ) * (5.0 + uSubBass * 10.0 + uLowMid * 6.0);\r
6958
- \r
6959
- vec3 turbulence = vec3(\r
6960
- snoise(noisePos * 2.5 + t * 0.18),\r
6961
- snoise(noisePos * 2.5 - t * 0.14),\r
6962
- snoise(noisePos * 2.5 + t * 0.12)\r
6963
- ) * (uHigh * 5.0 + uSpectralFlux * 8.0);\r
6964
- \r
6965
- vec3 offset = drift + turbulence;\r
6966
- \r
6967
- // Orbital drift \u2014 vortex-aware, modulated by bass\r
6968
- float orbitSpeed = 0.12 + instanceRandom.x * 0.15 + uBass * 0.08;\r
6969
- float angle = uTime * orbitSpeed + instancePhase;\r
6970
- float orbitRadius = length(instancePosition.xz) * (1.0 + uSubBass * 0.25);\r
6971
- // Vortex coupling \u2014 secondary shell rotates with the main field vortex\r
6972
- float vortexInfluence = uVortexStrength * uBass * 0.4;\r
6973
- float vAngle = angle + vortexInfluence;\r
6974
- vec3 orbit = vec3(\r
6975
- cos(vAngle) * orbitRadius - instancePosition.x,\r
6976
- sin(uTime * 0.5 + instancePhase) * (uMid * 4.0 + uLowMid * 2.0),\r
6977
- sin(vAngle) * orbitRadius - instancePosition.z\r
6978
- ) * 0.07;\r
6979
- \r
6949
+ float t = uTime * 0.45;
6950
+
6951
+ // Multi-scale noise flow \u2014 low freqs drive large drift, high freqs add fine turbulence
6952
+ vec3 noisePos = instancePosition * 0.035 + instanceRandom;
6953
+ vec3 drift = vec3(
6954
+ snoise(noisePos + t * 0.06),
6955
+ snoise(noisePos * 0.9 - t * 0.05),
6956
+ snoise(noisePos * 1.1 + t * 0.04)
6957
+ ) * (5.0 + uSubBass * 10.0 + uLowMid * 6.0);
6958
+
6959
+ vec3 turbulence = vec3(
6960
+ snoise(noisePos * 2.5 + t * 0.18),
6961
+ snoise(noisePos * 2.5 - t * 0.14),
6962
+ snoise(noisePos * 2.5 + t * 0.12)
6963
+ ) * (uHigh * 5.0 + uSpectralFlux * 8.0);
6964
+
6965
+ vec3 offset = drift + turbulence;
6966
+
6967
+ // Orbital drift \u2014 vortex-aware, modulated by bass
6968
+ float orbitSpeed = 0.12 + instanceRandom.x * 0.15 + uBass * 0.08;
6969
+ float angle = uTime * orbitSpeed + instancePhase;
6970
+ float orbitRadius = length(instancePosition.xz) * (1.0 + uSubBass * 0.25);
6971
+ // Vortex coupling \u2014 secondary shell rotates with the main field vortex
6972
+ float vortexInfluence = uVortexStrength * uBass * 0.4;
6973
+ float vAngle = angle + vortexInfluence;
6974
+ vec3 orbit = vec3(
6975
+ cos(vAngle) * orbitRadius - instancePosition.x,
6976
+ sin(uTime * 0.5 + instancePhase) * (uMid * 4.0 + uLowMid * 2.0),
6977
+ sin(vAngle) * orbitRadius - instancePosition.z
6978
+ ) * 0.07;
6979
+
6980
6980
  vec3 newPos = shapedPosition(instanceRandom) + offset + orbit;
6981
- \r
6981
+
6982
6982
  // Transients and sub-bass move the atmosphere in depth, not away from the centre.
6983
6983
  newPos.z += uOnsetEnergy * 14.0 * (0.6 + instanceRandom.z * 0.4);
6984
6984
  newPos.z += sin(uTime * 1.2 + instancePhase) * uSubBass * 5.0;
6985
- \r
6986
- vec4 mvPosition = modelViewMatrix * vec4(newPos, 1.0);\r
6987
- gl_Position = projectionMatrix * mvPosition;\r
6988
- \r
6989
- // Size: driven by different bands than the main system\r
6990
- // Sparkle on ultraHigh, swell on subBass, punch on beatEnergy\r
6991
- float sparkle = 1.0 + (instanceRandom.x > 0.85 ? sin(uTime * 15.0 + instanceRandom.y * 200.0) * uUltraHigh * 3.0 : 0.0);\r
6992
- float swell = 1.0 + uSubBass * 0.6 + uLowMid * 0.3;\r
6993
- float size = (1.1 + uBeatEnergy * 0.7) * swell * sparkle;\r
6994
- gl_PointSize = clamp(size * uPixelRatio * (55.0 / -mvPosition.z), 0.5, 55.0);\r
6995
- \r
6996
- // Colour: spectral centroid shifts hue, highMid brightens toward white\r
6997
- float centroidMix = clamp(uSpectralCentroid * 2.2 + uHighMid * 0.4, 0.0, 1.0);\r
6998
- vColor = mix(uColor1, uColor2, centroidMix);\r
6999
- // SubBass warmth \u2014 hint of deep colour saturation on heavy lows\r
7000
- vColor = mix(vColor, vColor * 1.3, uSubBass * 0.4);\r
7001
- // Beat flash \u2014 brief white-hot core\r
7002
- vColor = mix(vColor, vec3(1.0), uBeatEnergy * 0.25);\r
7003
- \r
7004
- // Alpha: outer shell fades at edge, mid-frequency presence sustains visibility\r
6985
+
6986
+ vec4 mvPosition = modelViewMatrix * vec4(newPos, 1.0);
6987
+ gl_Position = projectionMatrix * mvPosition;
6988
+
6989
+ // Size: driven by different bands than the main system
6990
+ // Sparkle on ultraHigh, swell on subBass, punch on beatEnergy
6991
+ float sparkle = 1.0 + (instanceRandom.x > 0.85 ? sin(uTime * 15.0 + instanceRandom.y * 200.0) * uUltraHigh * 3.0 : 0.0);
6992
+ float swell = 1.0 + uSubBass * 0.6 + uLowMid * 0.3;
6993
+ float size = (1.1 + uBeatEnergy * 0.7) * swell * sparkle;
6994
+ gl_PointSize = clamp(size * uPixelRatio * (55.0 / -mvPosition.z), 0.5, 55.0);
6995
+
6996
+ // Colour: spectral centroid shifts hue, highMid brightens toward white
6997
+ float centroidMix = clamp(uSpectralCentroid * 2.2 + uHighMid * 0.4, 0.0, 1.0);
6998
+ vColor = mix(uColor1, uColor2, centroidMix);
6999
+ // SubBass warmth \u2014 hint of deep colour saturation on heavy lows
7000
+ vColor = mix(vColor, vColor * 1.3, uSubBass * 0.4);
7001
+ // Beat flash \u2014 brief white-hot core
7002
+ vColor = mix(vColor, vec3(1.0), uBeatEnergy * 0.25);
7003
+
7004
+ // Alpha: outer shell fades at edge, mid-frequency presence sustains visibility
7005
7005
  float edgeFade = 1.0 - smoothstep(0.92, 1.05, max(abs(newPos.x) / (uViewportAspect * uViewportHalfHeight * 0.82), abs(newPos.y) / (uViewportHalfHeight * 0.82)));
7006
- vAlpha = edgeFade * (0.45 + uMid * 0.35 + uLowMid * 0.2) * sparkle;\r
7007
- \r
7008
- vEnergy = uBeatEnergy * 0.4 + uHighMid * 0.3 + uOnsetEnergy * 0.3;\r
7009
- vDepth = clamp(-mvPosition.z / 160.0, 0.0, 1.0);\r
7010
- }\r
7011
- \`;\r
7012
- \r
7013
- const secondaryFragmentShader = \`\r
7014
- varying vec3 vColor;\r
7015
- varying float vAlpha;\r
7016
- varying float vEnergy;\r
7017
- varying float vDepth;\r
7018
- \r
7019
- void main() {\r
7020
- vec2 xy = gl_PointCoord.xy - vec2(0.5);\r
7021
- float r = length(xy);\r
7022
- if (r > 0.5) discard;\r
7023
- \r
7024
- // Wider, softer glow than main particles \u2014 atmospheric haze\r
7025
- float glow = exp(-r * r * 8.0);\r
7026
- float softGlow = exp(-r * r * 3.0) * 0.4;\r
7027
- float core = smoothstep(0.18 - vEnergy * 0.06, 0.10, r);\r
7028
- \r
7029
- vec3 finalColor = mix(vColor, vec3(1.0, 0.97, 0.92), core * 0.55);\r
7030
- // Depth haze \u2014 far particles shift slightly cooler/dimmer\r
7031
- finalColor = mix(finalColor, vec3(0.02, 0.03, 0.06), vDepth * 0.35);\r
7032
- \r
7033
- float finalAlpha = clamp(vAlpha * (glow * 0.65 + softGlow + core * 0.35), 0.0, 1.0);\r
7034
- gl_FragColor = vec4(finalColor, finalAlpha);\r
7035
- }\r
7036
- \`;\r
7037
- \r
7038
- // Create instanced geometry for secondary particles\r
7039
- const secondaryGeometry = new THREE.InstancedBufferGeometry();\r
7040
- \r
7041
- // Base geometry (single point)\r
7042
- const basePositions = new Float32Array([0, 0, 0]);\r
7043
- secondaryGeometry.setAttribute('position', new THREE.BufferAttribute(basePositions, 3));\r
7044
- \r
7045
- // Instance attributes\r
7046
- const instancePositions = new Float32Array(CONFIG.secondaryParticles * 3);\r
7047
- const instanceRandoms = new Float32Array(CONFIG.secondaryParticles * 3);\r
7048
- const instancePhases = new Float32Array(CONFIG.secondaryParticles);\r
7049
- \r
7050
- for (let i = 0; i < CONFIG.secondaryParticles; i++) {\r
7051
- // Distribute in outer shell\r
7052
- const r = CONFIG.fieldRadius * (0.65 + Math.random() * 0.45);\r
7053
- const theta = Math.acos(2 * Math.random() - 1);\r
7054
- const phi = Math.random() * Math.PI * 2;\r
7055
- \r
7056
- instancePositions[i * 3] = r * Math.sin(theta) * Math.cos(phi);\r
7057
- instancePositions[i * 3 + 1] = r * Math.sin(theta) * Math.sin(phi);\r
7058
- instancePositions[i * 3 + 2] = r * Math.cos(theta);\r
7059
- \r
7060
- instanceRandoms[i * 3] = Math.random();\r
7061
- instanceRandoms[i * 3 + 1] = Math.random();\r
7062
- instanceRandoms[i * 3 + 2] = Math.random();\r
7063
- \r
7064
- instancePhases[i] = Math.random() * Math.PI * 2;\r
7065
- }\r
7066
- \r
7067
- secondaryGeometry.setAttribute('instancePosition', new THREE.InstancedBufferAttribute(instancePositions, 3));\r
7068
- secondaryGeometry.setAttribute('instanceRandom', new THREE.InstancedBufferAttribute(instanceRandoms, 3));\r
7069
- secondaryGeometry.setAttribute('instancePhase', new THREE.InstancedBufferAttribute(instancePhases, 1));\r
7070
- \r
7071
- const secondaryMaterial = new THREE.ShaderMaterial({\r
7072
- vertexShader: secondaryVertexShader,\r
7073
- fragmentShader: secondaryFragmentShader,\r
7074
- uniforms: {\r
7075
- uTime: { value: 0 },\r
7076
- uPixelRatio: { value: STATE.pixelRatio },\r
7077
- uSubBass: { value: 0 },\r
7078
- uBass: { value: 0 },\r
7079
- uLowMid: { value: 0 },\r
7080
- uMid: { value: 0 },\r
7081
- uHighMid: { value: 0 },\r
7082
- uHigh: { value: 0 },\r
7083
- uUltraHigh: { value: 0 },\r
7084
- uBeatEnergy: { value: 0 },\r
7085
- uSpectralCentroid: { value: 0 },\r
7086
- uSpectralFlux: { value: 0 },\r
7087
- uOnsetEnergy: { value: 0 },\r
7088
- uVortexStrength: { value: STATE.vortexStrength },\r
7089
- uColor1: { value: new THREE.Color(def.sc1) },\r
7090
- uColor2: { value: new THREE.Color(def.sc2) },\r
7006
+ vAlpha = edgeFade * (0.45 + uMid * 0.35 + uLowMid * 0.2) * sparkle;
7007
+
7008
+ vEnergy = uBeatEnergy * 0.4 + uHighMid * 0.3 + uOnsetEnergy * 0.3;
7009
+ vDepth = clamp(-mvPosition.z / 160.0, 0.0, 1.0);
7010
+ }
7011
+ \`;
7012
+
7013
+ const secondaryFragmentShader = \`
7014
+ varying vec3 vColor;
7015
+ varying float vAlpha;
7016
+ varying float vEnergy;
7017
+ varying float vDepth;
7018
+
7019
+ void main() {
7020
+ vec2 xy = gl_PointCoord.xy - vec2(0.5);
7021
+ float r = length(xy);
7022
+ if (r > 0.5) discard;
7023
+
7024
+ // Wider, softer glow than main particles \u2014 atmospheric haze
7025
+ float glow = exp(-r * r * 8.0);
7026
+ float softGlow = exp(-r * r * 3.0) * 0.4;
7027
+ float core = smoothstep(0.18 - vEnergy * 0.06, 0.10, r);
7028
+
7029
+ vec3 finalColor = mix(vColor, vec3(1.0, 0.97, 0.92), core * 0.55);
7030
+ // Depth haze \u2014 far particles shift slightly cooler/dimmer
7031
+ finalColor = mix(finalColor, vec3(0.02, 0.03, 0.06), vDepth * 0.35);
7032
+
7033
+ float finalAlpha = clamp(vAlpha * (glow * 0.65 + softGlow + core * 0.35), 0.0, 1.0);
7034
+ gl_FragColor = vec4(finalColor, finalAlpha);
7035
+ }
7036
+ \`;
7037
+
7038
+ // Create instanced geometry for secondary particles
7039
+ const secondaryGeometry = new THREE.InstancedBufferGeometry();
7040
+
7041
+ // Base geometry (single point)
7042
+ const basePositions = new Float32Array([0, 0, 0]);
7043
+ secondaryGeometry.setAttribute('position', new THREE.BufferAttribute(basePositions, 3));
7044
+
7045
+ // Instance attributes
7046
+ const instancePositions = new Float32Array(CONFIG.secondaryParticles * 3);
7047
+ const instanceRandoms = new Float32Array(CONFIG.secondaryParticles * 3);
7048
+ const instancePhases = new Float32Array(CONFIG.secondaryParticles);
7049
+
7050
+ for (let i = 0; i < CONFIG.secondaryParticles; i++) {
7051
+ // Distribute in outer shell
7052
+ const r = CONFIG.fieldRadius * (0.65 + Math.random() * 0.45);
7053
+ const theta = Math.acos(2 * Math.random() - 1);
7054
+ const phi = Math.random() * Math.PI * 2;
7055
+
7056
+ instancePositions[i * 3] = r * Math.sin(theta) * Math.cos(phi);
7057
+ instancePositions[i * 3 + 1] = r * Math.sin(theta) * Math.sin(phi);
7058
+ instancePositions[i * 3 + 2] = r * Math.cos(theta);
7059
+
7060
+ instanceRandoms[i * 3] = Math.random();
7061
+ instanceRandoms[i * 3 + 1] = Math.random();
7062
+ instanceRandoms[i * 3 + 2] = Math.random();
7063
+
7064
+ instancePhases[i] = Math.random() * Math.PI * 2;
7065
+ }
7066
+
7067
+ secondaryGeometry.setAttribute('instancePosition', new THREE.InstancedBufferAttribute(instancePositions, 3));
7068
+ secondaryGeometry.setAttribute('instanceRandom', new THREE.InstancedBufferAttribute(instanceRandoms, 3));
7069
+ secondaryGeometry.setAttribute('instancePhase', new THREE.InstancedBufferAttribute(instancePhases, 1));
7070
+
7071
+ const secondaryMaterial = new THREE.ShaderMaterial({
7072
+ vertexShader: secondaryVertexShader,
7073
+ fragmentShader: secondaryFragmentShader,
7074
+ uniforms: {
7075
+ uTime: { value: 0 },
7076
+ uPixelRatio: { value: STATE.pixelRatio },
7077
+ uSubBass: { value: 0 },
7078
+ uBass: { value: 0 },
7079
+ uLowMid: { value: 0 },
7080
+ uMid: { value: 0 },
7081
+ uHighMid: { value: 0 },
7082
+ uHigh: { value: 0 },
7083
+ uUltraHigh: { value: 0 },
7084
+ uBeatEnergy: { value: 0 },
7085
+ uSpectralCentroid: { value: 0 },
7086
+ uSpectralFlux: { value: 0 },
7087
+ uOnsetEnergy: { value: 0 },
7088
+ uVortexStrength: { value: STATE.vortexStrength },
7089
+ uColor1: { value: new THREE.Color(def.sc1) },
7090
+ uColor2: { value: new THREE.Color(def.sc2) },
7091
7091
  uViewportAspect: { value: window.innerWidth / window.innerHeight },
7092
7092
  uViewportHalfHeight: { value: Math.tan(camera.fov * Math.PI / 360) * CONFIG.defaultZoom }
7093
- },\r
7094
- transparent: true,\r
7095
- depthWrite: false,\r
7096
- blending: THREE.AdditiveBlending\r
7097
- });\r
7098
- \r
7099
- const secondaryPoints = new THREE.Points(secondaryGeometry, secondaryMaterial);\r
7100
- scene.add(secondaryPoints);\r
7101
- \r
7093
+ },
7094
+ transparent: true,
7095
+ depthWrite: false,
7096
+ blending: THREE.AdditiveBlending
7097
+ });
7098
+
7099
+ const secondaryPoints = new THREE.Points(secondaryGeometry, secondaryMaterial);
7100
+ scene.add(secondaryPoints);
7101
+
7102
7102
  // Expose secondary-layer controls to the host component.
7103
- STATE.secondaryEnabled = true;\r
7104
- STATE.secondaryOpacity = 1.0;\r
7105
- \r
7106
- // \u2550\u2550\u2550\u2550\u2550\u2550\u2550 PARTICLE TRAIL SYSTEM \u2550\u2550\u2550\u2550\u2550\u2550\u2550\r
7103
+ STATE.secondaryEnabled = true;
7104
+ STATE.secondaryOpacity = 1.0;
7105
+
7106
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550 PARTICLE TRAIL SYSTEM \u2550\u2550\u2550\u2550\u2550\u2550\u2550
7107
7107
  const particleSeeds = geometry.getAttribute('aRandom');
7108
7108
  function fieldParticlePosition(index, target) {
7109
7109
  const halfHeight = material.uniforms.uViewportHalfHeight.value * 0.78;
@@ -7116,7 +7116,7 @@ void main() {\r
7116
7116
  }
7117
7117
 
7118
7118
  class TrailSystem {
7119
- constructor(count, length) {\r
7119
+ constructor(count, length) {
7120
7120
  this.trails = [];
7121
7121
  this.meshes = [];
7122
7122
  this.nextPosition = new THREE.Vector3();
@@ -7131,41 +7131,41 @@ void main() {\r
7131
7131
  const trail = {
7132
7132
  positions: Array.from({ length }, () => start.clone()),
7133
7133
  anchor,
7134
- phase: Math.random() * Math.PI * 2,\r
7135
- speed: 0.3 + Math.random() * 0.7,\r
7136
- color: new THREE.Color().setHSL(Math.random(), 0.7, 0.6)\r
7137
- };\r
7138
- this.trails.push(trail);\r
7139
- \r
7140
- const geom = new THREE.BufferGeometry();\r
7141
- const positions = new Float32Array(length * 3);\r
7142
- const alphas = new Float32Array(length);\r
7143
- for (let j = 0; j < length; j++) alphas[j] = 1.0 - j / length;\r
7144
- \r
7145
- geom.setAttribute('position', new THREE.BufferAttribute(positions, 3));\r
7146
- geom.setAttribute('alpha', new THREE.BufferAttribute(alphas, 1));\r
7147
- \r
7148
- const mat = new THREE.ShaderMaterial({\r
7149
- vertexShader: \`attribute float alpha; varying float vAlpha; void main() { vAlpha = alpha; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }\`,\r
7150
- fragmentShader: \`uniform vec3 uColor; uniform float uOpacity, uBeat; varying float vAlpha; void main() { gl_FragColor = vec4(uColor * (1.0 + uBeat * 0.3), vAlpha * uOpacity * 0.6 * (1.0 + uBeat * 0.5)); }\`,\r
7151
- uniforms: { uColor: { value: trail.color }, uOpacity: { value: 0.4 }, uBeat: { value: 0 } },\r
7152
- transparent: true, depthWrite: false, blending: THREE.AdditiveBlending\r
7153
- });\r
7154
- \r
7155
- const line = new THREE.Line(geom, mat);\r
7156
- this.meshes.push(line);\r
7157
- scene.add(line);\r
7158
- }\r
7159
- }\r
7160
- \r
7134
+ phase: Math.random() * Math.PI * 2,
7135
+ speed: 0.3 + Math.random() * 0.7,
7136
+ color: new THREE.Color().setHSL(Math.random(), 0.7, 0.6)
7137
+ };
7138
+ this.trails.push(trail);
7139
+
7140
+ const geom = new THREE.BufferGeometry();
7141
+ const positions = new Float32Array(length * 3);
7142
+ const alphas = new Float32Array(length);
7143
+ for (let j = 0; j < length; j++) alphas[j] = 1.0 - j / length;
7144
+
7145
+ geom.setAttribute('position', new THREE.BufferAttribute(positions, 3));
7146
+ geom.setAttribute('alpha', new THREE.BufferAttribute(alphas, 1));
7147
+
7148
+ const mat = new THREE.ShaderMaterial({
7149
+ vertexShader: \`attribute float alpha; varying float vAlpha; void main() { vAlpha = alpha; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }\`,
7150
+ fragmentShader: \`uniform vec3 uColor; uniform float uOpacity, uBeat; varying float vAlpha; void main() { gl_FragColor = vec4(uColor * (1.0 + uBeat * 0.3), vAlpha * uOpacity * 0.6 * (1.0 + uBeat * 0.5)); }\`,
7151
+ uniforms: { uColor: { value: trail.color }, uOpacity: { value: 0.4 }, uBeat: { value: 0 } },
7152
+ transparent: true, depthWrite: false, blending: THREE.AdditiveBlending
7153
+ });
7154
+
7155
+ const line = new THREE.Line(geom, mat);
7156
+ this.meshes.push(line);
7157
+ scene.add(line);
7158
+ }
7159
+ }
7160
+
7161
7161
  update(time, audio, fieldColor) {
7162
7162
  const halfHeight = material.uniforms.uViewportHalfHeight.value * 0.78;
7163
7163
  const halfWidth = halfHeight * material.uniforms.uViewportAspect.value;
7164
7164
  for (let i = 0; i < this.trails.length; i++) {
7165
- const trail = this.trails[i], mesh = this.meshes[i];\r
7165
+ const trail = this.trails[i], mesh = this.meshes[i];
7166
7166
  mesh.visible = STATE.particleTrailsEnabled && i < this.trails.length * STATE.qualityLevel;
7167
- if (!mesh.visible) continue;\r
7168
- \r
7167
+ if (!mesh.visible) continue;
7168
+
7169
7169
  const angle = time * 0.5 * trail.speed + trail.phase;
7170
7170
  const newPos = this.nextPosition.set(
7171
7171
  trail.anchor.x * halfWidth + Math.cos(angle) * (4 + audio.bass * 8),
@@ -7174,86 +7174,86 @@ void main() {\r
7174
7174
  );
7175
7175
  newPos.x = THREE.MathUtils.clamp(newPos.x, -halfWidth * 0.94, halfWidth * 0.94);
7176
7176
  newPos.y = THREE.MathUtils.clamp(newPos.y, -halfHeight * 0.94, halfHeight * 0.94);
7177
- \r
7178
- for (let j = trail.positions.length - 1; j > 0; j--) trail.positions[j].copy(trail.positions[j - 1]);\r
7179
- trail.positions[0].copy(newPos);\r
7180
- \r
7181
- const posAttr = mesh.geometry.getAttribute('position');\r
7182
- for (let j = 0; j < trail.positions.length; j++) {\r
7183
- posAttr.setXYZ(j, trail.positions[j].x, trail.positions[j].y, trail.positions[j].z);\r
7184
- }\r
7185
- posAttr.needsUpdate = true;\r
7186
- \r
7187
- mesh.material.uniforms.uBeat.value = audio.beat;\r
7188
- mesh.material.uniforms.uOpacity.value = STATE.particleTrailOpacity;\r
7189
- if (fieldColor) trail.color.lerp(fieldColor, 0.01);\r
7190
- }\r
7191
- }\r
7192
- }\r
7193
- \r
7194
- const trailSystem = new TrailSystem(50, 12);\r
7195
- \r
7196
- // \u2550\u2550\u2550\u2550\u2550\u2550\u2550 PARTICLE CONNECTIONS (Force Network) \u2550\u2550\u2550\u2550\u2550\u2550\u2550\r
7197
- const connectionCount = 500; // Number of particles to check for connections\r
7198
- const maxConnections = 800; // Maximum line segments\r
7199
- const connectionGeometry = new THREE.BufferGeometry();\r
7200
- const connectionPositions = new Float32Array(maxConnections * 6); // 2 points per line, 3 coords each\r
7201
- const connectionAlphas = new Float32Array(maxConnections * 2); // Alpha per vertex\r
7202
- connectionGeometry.setAttribute('position', new THREE.BufferAttribute(connectionPositions, 3));\r
7203
- connectionGeometry.setAttribute('alpha', new THREE.BufferAttribute(connectionAlphas, 1));\r
7204
- connectionGeometry.setDrawRange(0, 0);\r
7205
- \r
7206
- const connectionMaterial = new THREE.ShaderMaterial({\r
7207
- vertexShader: \`\r
7208
- attribute float alpha;\r
7209
- varying float vAlpha;\r
7177
+
7178
+ for (let j = trail.positions.length - 1; j > 0; j--) trail.positions[j].copy(trail.positions[j - 1]);
7179
+ trail.positions[0].copy(newPos);
7180
+
7181
+ const posAttr = mesh.geometry.getAttribute('position');
7182
+ for (let j = 0; j < trail.positions.length; j++) {
7183
+ posAttr.setXYZ(j, trail.positions[j].x, trail.positions[j].y, trail.positions[j].z);
7184
+ }
7185
+ posAttr.needsUpdate = true;
7186
+
7187
+ mesh.material.uniforms.uBeat.value = audio.beat;
7188
+ mesh.material.uniforms.uOpacity.value = STATE.particleTrailOpacity;
7189
+ if (fieldColor) trail.color.lerp(fieldColor, 0.01);
7190
+ }
7191
+ }
7192
+ }
7193
+
7194
+ const trailSystem = new TrailSystem(50, 12);
7195
+
7196
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550 PARTICLE CONNECTIONS (Force Network) \u2550\u2550\u2550\u2550\u2550\u2550\u2550
7197
+ const connectionCount = 500; // Number of particles to check for connections
7198
+ const maxConnections = 800; // Maximum line segments
7199
+ const connectionGeometry = new THREE.BufferGeometry();
7200
+ const connectionPositions = new Float32Array(maxConnections * 6); // 2 points per line, 3 coords each
7201
+ const connectionAlphas = new Float32Array(maxConnections * 2); // Alpha per vertex
7202
+ connectionGeometry.setAttribute('position', new THREE.BufferAttribute(connectionPositions, 3));
7203
+ connectionGeometry.setAttribute('alpha', new THREE.BufferAttribute(connectionAlphas, 1));
7204
+ connectionGeometry.setDrawRange(0, 0);
7205
+
7206
+ const connectionMaterial = new THREE.ShaderMaterial({
7207
+ vertexShader: \`
7208
+ attribute float alpha;
7209
+ varying float vAlpha;
7210
7210
  void main() {
7211
7211
  vAlpha = alpha;
7212
7212
  vec4 mvPos = modelViewMatrix * vec4(position, 1.0);
7213
7213
  gl_Position = projectionMatrix * mvPos;
7214
- }\r
7215
- \`,\r
7216
- fragmentShader: \`\r
7217
- uniform vec3 uColor;\r
7218
- uniform vec3 uColor2;\r
7219
- uniform float uOpacity;\r
7220
- uniform float uBeatEnergy;\r
7221
- uniform float uHighMid;\r
7222
- uniform float uSpectralCentroid;\r
7223
- varying float vAlpha;\r
7214
+ }
7215
+ \`,
7216
+ fragmentShader: \`
7217
+ uniform vec3 uColor;
7218
+ uniform vec3 uColor2;
7219
+ uniform float uOpacity;
7220
+ uniform float uBeatEnergy;
7221
+ uniform float uHighMid;
7222
+ uniform float uSpectralCentroid;
7223
+ varying float vAlpha;
7224
7224
  void main() {
7225
7225
  // Blend hue with spectral centroid (brighter/higher = more c2)
7226
7226
  vec3 color = mix(uColor, uColor2, clamp(uSpectralCentroid * 2.0, 0.0, 1.0));
7227
- // Beat pulse flash\r
7228
- float beatFlash = 1.0 + uBeatEnergy * 1.2;\r
7229
- color *= beatFlash;\r
7230
- // Per-line alpha reacts to highMid (attack/presence energy)\r
7231
- float lineAlpha = vAlpha * uOpacity * (0.4 + uHighMid * 0.5 + uBeatEnergy * 0.4);\r
7232
- gl_FragColor = vec4(color, clamp(lineAlpha, 0.0, 1.0));\r
7233
- }\r
7234
- \`,\r
7235
- uniforms: {\r
7236
- uColor: { value: new THREE.Color(def.c2) },\r
7237
- uColor2: { value: new THREE.Color(def.c1) },\r
7238
- uOpacity: { value: 1.0 },\r
7239
- uBeatEnergy: { value: 0 },\r
7240
- uHighMid: { value: 0 },\r
7241
- uSpectralCentroid: { value: 0 }\r
7242
- },\r
7243
- transparent: true,\r
7244
- depthWrite: false,\r
7245
- blending: THREE.AdditiveBlending\r
7246
- });\r
7247
- \r
7248
- const connectionMesh = new THREE.LineSegments(connectionGeometry, connectionMaterial);\r
7249
- scene.add(connectionMesh);\r
7250
- \r
7227
+ // Beat pulse flash
7228
+ float beatFlash = 1.0 + uBeatEnergy * 1.2;
7229
+ color *= beatFlash;
7230
+ // Per-line alpha reacts to highMid (attack/presence energy)
7231
+ float lineAlpha = vAlpha * uOpacity * (0.4 + uHighMid * 0.5 + uBeatEnergy * 0.4);
7232
+ gl_FragColor = vec4(color, clamp(lineAlpha, 0.0, 1.0));
7233
+ }
7234
+ \`,
7235
+ uniforms: {
7236
+ uColor: { value: new THREE.Color(def.c2) },
7237
+ uColor2: { value: new THREE.Color(def.c1) },
7238
+ uOpacity: { value: 1.0 },
7239
+ uBeatEnergy: { value: 0 },
7240
+ uHighMid: { value: 0 },
7241
+ uSpectralCentroid: { value: 0 }
7242
+ },
7243
+ transparent: true,
7244
+ depthWrite: false,
7245
+ blending: THREE.AdditiveBlending
7246
+ });
7247
+
7248
+ const connectionMesh = new THREE.LineSegments(connectionGeometry, connectionMaterial);
7249
+ scene.add(connectionMesh);
7250
+
7251
7251
  // Particle indices to track for connections \u2014 refreshed periodically
7252
7252
  const trackedParticles = [];
7253
7253
  const positions = Array.from({ length: connectionCount }, () => new THREE.Vector3());
7254
- let connectionRefreshCounter = 0;\r
7254
+ let connectionRefreshCounter = 0;
7255
7255
  const CONNECTION_REFRESH_INTERVAL = 40; // 20 Hz updates preserve the original ~2 s selection cycle
7256
- \r
7256
+
7257
7257
  function refreshTrackedParticles() {
7258
7258
  trackedParticles.length = 0;
7259
7259
  const visibleParticles = Math.max(1, geometry.drawRange.count);
@@ -7261,7 +7261,7 @@ void main() {\r
7261
7261
  const trackedCount = Math.max(2, Math.floor(connectionCount * Math.min(1, STATE.qualityLevel, visibleFraction)));
7262
7262
  for (let i = 0; i < trackedCount; i++) {
7263
7263
  trackedParticles.push(Math.floor(Math.random() * visibleParticles));
7264
- }\r
7264
+ }
7265
7265
  }
7266
7266
  refreshTrackedParticles();
7267
7267
  let lastConnectionState = null;
@@ -7274,395 +7274,395 @@ void main() {\r
7274
7274
  // Refresh particle selection every N frames \u2014 network evolves over time
7275
7275
  connectionRefreshCounter++;
7276
7276
  if (forceRefresh || connectionRefreshCounter >= CONNECTION_REFRESH_INTERVAL) {
7277
- refreshTrackedParticles();\r
7278
- connectionRefreshCounter = 0;\r
7279
- }\r
7280
- \r
7277
+ refreshTrackedParticles();
7278
+ connectionRefreshCounter = 0;
7279
+ }
7280
+
7281
7281
  // Mirror the shader's screen-fitted seed mapping for the line systems.
7282
7282
  for (let i = 0; i < trackedParticles.length; i++) {
7283
7283
  fieldParticlePosition(trackedParticles[i], positions[i]);
7284
- }\r
7285
- \r
7286
- // Threshold breathes with bass, tightens on high energy\r
7287
- const effectiveThreshold = threshold * (1.0 + bassBoost * 0.6) * (1.0 - highMid * 0.15);\r
7288
- const thresholdSq = effectiveThreshold * effectiveThreshold;\r
7289
- \r
7290
- // Dynamic connection cap \u2014 expands with mid+highMid energy\r
7291
- const dynamicMax = Math.floor(maxConnections * (0.3 + (mid + highMid) * 0.7));\r
7292
- \r
7293
- let connIdx = 0;\r
7284
+ }
7285
+
7286
+ // Threshold breathes with bass, tightens on high energy
7287
+ const effectiveThreshold = threshold * (1.0 + bassBoost * 0.6) * (1.0 - highMid * 0.15);
7288
+ const thresholdSq = effectiveThreshold * effectiveThreshold;
7289
+
7290
+ // Dynamic connection cap \u2014 expands with mid+highMid energy
7291
+ const dynamicMax = Math.floor(maxConnections * (0.3 + (mid + highMid) * 0.7));
7292
+
7293
+ let connIdx = 0;
7294
7294
  for (let i = 0; i < trackedParticles.length && connIdx < dynamicMax; i++) {
7295
7295
  for (let j = i + 1; j < trackedParticles.length && connIdx < dynamicMax; j++) {
7296
- const distSq = positions[i].distanceToSquared(positions[j]);\r
7297
- if (distSq < thresholdSq && distSq > 0.1) {\r
7298
- const dist = Math.sqrt(distSq);\r
7296
+ const distSq = positions[i].distanceToSquared(positions[j]);
7297
+ if (distSq < thresholdSq && distSq > 0.1) {
7298
+ const dist = Math.sqrt(distSq);
7299
7299
  const alpha = (1.0 - dist / effectiveThreshold) * 0.8;
7300
- \r
7301
- connectionPositions[connIdx * 6 + 0] = positions[i].x;\r
7302
- connectionPositions[connIdx * 6 + 1] = positions[i].y;\r
7303
- connectionPositions[connIdx * 6 + 2] = positions[i].z;\r
7304
- connectionPositions[connIdx * 6 + 3] = positions[j].x;\r
7305
- connectionPositions[connIdx * 6 + 4] = positions[j].y;\r
7306
- connectionPositions[connIdx * 6 + 5] = positions[j].z;\r
7307
- \r
7308
- connectionAlphas[connIdx * 2 + 0] = alpha;\r
7309
- connectionAlphas[connIdx * 2 + 1] = alpha;\r
7310
- \r
7311
- connIdx++;\r
7312
- }\r
7313
- }\r
7314
- }\r
7315
- \r
7316
- connectionGeometry.attributes.position.needsUpdate = true;\r
7317
- connectionGeometry.attributes.alpha.needsUpdate = true;\r
7318
- connectionGeometry.setDrawRange(0, connIdx * 2);\r
7319
- }\r
7320
- \r
7321
- // \u2550\u2550\u2550\u2550\u2550\u2550\u2550 NETWORK CRAWLERS \u2550\u2550\u2550\u2550\u2550\u2550\u2550\r
7322
- const CRAWLER_COUNT = 24; // concurrent crawlers\r
7323
- const CRAWLER_TAIL = 10; // trail positions per crawler\r
7324
- const crawlerMaxSegs = CRAWLER_COUNT * (CRAWLER_TAIL - 1);\r
7325
- \r
7326
- const crawlerGeo = new THREE.BufferGeometry();\r
7327
- const crawlerPos = new Float32Array(crawlerMaxSegs * 6);\r
7328
- const crawlerAlp = new Float32Array(crawlerMaxSegs * 2);\r
7329
- crawlerGeo.setAttribute('position', new THREE.BufferAttribute(crawlerPos, 3));\r
7330
- crawlerGeo.setAttribute('alpha', new THREE.BufferAttribute(crawlerAlp, 1));\r
7331
- crawlerGeo.setDrawRange(0, 0);\r
7332
- \r
7333
- const crawlerMaterial = new THREE.ShaderMaterial({\r
7334
- vertexShader: \`\r
7335
- attribute float alpha;\r
7336
- varying float vAlpha;\r
7337
- void main() {\r
7338
- vAlpha = alpha;\r
7339
- gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\r
7340
- }\r
7341
- \`,\r
7342
- fragmentShader: \`\r
7343
- uniform vec3 uColor;\r
7344
- uniform float uBeatEnergy;\r
7345
- varying float vAlpha;\r
7346
- void main() {\r
7347
- vec3 c = uColor * (1.2 + uBeatEnergy * 1.5);\r
7348
- gl_FragColor = vec4(c, vAlpha);\r
7349
- }\r
7350
- \`,\r
7351
- uniforms: {\r
7352
- uColor: { value: new THREE.Color(def.c1) },\r
7353
- uBeatEnergy: { value: 0 }\r
7354
- },\r
7355
- transparent: true,\r
7356
- depthWrite: false,\r
7357
- blending: THREE.AdditiveBlending\r
7358
- });\r
7359
- \r
7360
- const crawlerMesh = new THREE.LineSegments(crawlerGeo, crawlerMaterial);\r
7361
- scene.add(crawlerMesh);\r
7362
- \r
7363
- // Initialise crawler agents\r
7364
- const crawlers = [];\r
7365
- const CRAWLER_MIN_STEP = 1.8; // min world-space distance before adding a tail point\r
7366
- for (let i = 0; i < CRAWLER_COUNT; i++) {\r
7367
- crawlers.push({\r
7368
- fromIdx: Math.floor(Math.random() * connectionCount),\r
7369
- toIdx: Math.floor(Math.random() * connectionCount),\r
7370
- t: Math.random(),\r
7371
- speed: 0.0015 + Math.random() * 0.002,\r
7372
- tail: [],\r
7373
- lastTailPos: null, // last world position a tail point was recorded\r
7374
- hue: Math.random()\r
7375
- });\r
7376
- }\r
7377
- \r
7300
+
7301
+ connectionPositions[connIdx * 6 + 0] = positions[i].x;
7302
+ connectionPositions[connIdx * 6 + 1] = positions[i].y;
7303
+ connectionPositions[connIdx * 6 + 2] = positions[i].z;
7304
+ connectionPositions[connIdx * 6 + 3] = positions[j].x;
7305
+ connectionPositions[connIdx * 6 + 4] = positions[j].y;
7306
+ connectionPositions[connIdx * 6 + 5] = positions[j].z;
7307
+
7308
+ connectionAlphas[connIdx * 2 + 0] = alpha;
7309
+ connectionAlphas[connIdx * 2 + 1] = alpha;
7310
+
7311
+ connIdx++;
7312
+ }
7313
+ }
7314
+ }
7315
+
7316
+ connectionGeometry.attributes.position.needsUpdate = true;
7317
+ connectionGeometry.attributes.alpha.needsUpdate = true;
7318
+ connectionGeometry.setDrawRange(0, connIdx * 2);
7319
+ }
7320
+
7321
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550 NETWORK CRAWLERS \u2550\u2550\u2550\u2550\u2550\u2550\u2550
7322
+ const CRAWLER_COUNT = 24; // concurrent crawlers
7323
+ const CRAWLER_TAIL = 10; // trail positions per crawler
7324
+ const crawlerMaxSegs = CRAWLER_COUNT * (CRAWLER_TAIL - 1);
7325
+
7326
+ const crawlerGeo = new THREE.BufferGeometry();
7327
+ const crawlerPos = new Float32Array(crawlerMaxSegs * 6);
7328
+ const crawlerAlp = new Float32Array(crawlerMaxSegs * 2);
7329
+ crawlerGeo.setAttribute('position', new THREE.BufferAttribute(crawlerPos, 3));
7330
+ crawlerGeo.setAttribute('alpha', new THREE.BufferAttribute(crawlerAlp, 1));
7331
+ crawlerGeo.setDrawRange(0, 0);
7332
+
7333
+ const crawlerMaterial = new THREE.ShaderMaterial({
7334
+ vertexShader: \`
7335
+ attribute float alpha;
7336
+ varying float vAlpha;
7337
+ void main() {
7338
+ vAlpha = alpha;
7339
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
7340
+ }
7341
+ \`,
7342
+ fragmentShader: \`
7343
+ uniform vec3 uColor;
7344
+ uniform float uBeatEnergy;
7345
+ varying float vAlpha;
7346
+ void main() {
7347
+ vec3 c = uColor * (1.2 + uBeatEnergy * 1.5);
7348
+ gl_FragColor = vec4(c, vAlpha);
7349
+ }
7350
+ \`,
7351
+ uniforms: {
7352
+ uColor: { value: new THREE.Color(def.c1) },
7353
+ uBeatEnergy: { value: 0 }
7354
+ },
7355
+ transparent: true,
7356
+ depthWrite: false,
7357
+ blending: THREE.AdditiveBlending
7358
+ });
7359
+
7360
+ const crawlerMesh = new THREE.LineSegments(crawlerGeo, crawlerMaterial);
7361
+ scene.add(crawlerMesh);
7362
+
7363
+ // Initialise crawler agents
7364
+ const crawlers = [];
7365
+ const CRAWLER_MIN_STEP = 1.8; // min world-space distance before adding a tail point
7366
+ for (let i = 0; i < CRAWLER_COUNT; i++) {
7367
+ crawlers.push({
7368
+ fromIdx: Math.floor(Math.random() * connectionCount),
7369
+ toIdx: Math.floor(Math.random() * connectionCount),
7370
+ t: Math.random(),
7371
+ speed: 0.0015 + Math.random() * 0.002,
7372
+ tail: [],
7373
+ lastTailPos: null, // last world position a tail point was recorded
7374
+ hue: Math.random()
7375
+ });
7376
+ }
7377
+
7378
7378
  function updateCrawlers(dt, highMid, beatEnergy, spectralCentroid) {
7379
7379
  // Helper: world position of a tracked particle
7380
7380
  function particlePos(idx) {
7381
7381
  return positions[idx % trackedParticles.length];
7382
- }\r
7383
- \r
7384
- // Helper: find nearest neighbour within threshold\r
7385
- function nearestNeighbour(fromIdx, excludeIdx) {\r
7386
- const origin = particlePos(fromIdx);\r
7387
- const threshold = STATE.connectionThreshold * (1.2 + highMid * 0.4);\r
7388
- const threshSq = threshold * threshold;\r
7389
- let bestDist = Infinity, bestIdx = -1;\r
7390
- const check = Math.min(trackedParticles.length, 80); // sample subset for perf\r
7391
- const start = Math.floor(Math.random() * (trackedParticles.length - check));\r
7392
- for (let k = start; k < start + check; k++) {\r
7393
- const ki = k % trackedParticles.length;\r
7394
- if (ki === fromIdx || ki === excludeIdx) continue;\r
7395
- const d = origin.distanceToSquared(particlePos(ki));\r
7396
- if (d < threshSq && d < bestDist) { bestDist = d; bestIdx = ki; }\r
7397
- }\r
7398
- // fallback: pick random if no neighbour in range\r
7399
- return bestIdx === -1 ? Math.floor(Math.random() * trackedParticles.length) : bestIdx;\r
7400
- }\r
7401
- \r
7402
- let segIdx = 0;\r
7403
- \r
7404
- for (let i = 0; i < crawlers.length; i++) {\r
7405
- const c = crawlers[i];\r
7406
- \r
7407
- // Speed reacts to highMid (attack energy) and beat\r
7408
- const spd = c.speed * (1.0 + highMid * 0.8 + beatEnergy * 0.5);\r
7409
- c.t += spd;\r
7410
- \r
7411
- // Reached target \u2014 advance to next node\r
7412
- if (c.t >= 1.0) {\r
7413
- c.t -= 1.0;\r
7414
- c.fromIdx = c.toIdx;\r
7415
- c.toIdx = nearestNeighbour(c.fromIdx, c.fromIdx);\r
7416
- // Occasionally teleport to keep crawlers spread across the field\r
7417
- if (Math.random() < 0.04) {\r
7418
- c.fromIdx = Math.floor(Math.random() * trackedParticles.length);\r
7419
- c.toIdx = nearestNeighbour(c.fromIdx, c.fromIdx);\r
7420
- c.tail = [];\r
7421
- c.lastTailPos = null;\r
7422
- }\r
7423
- }\r
7424
- \r
7425
- // Current head position\r
7426
- const from = particlePos(c.fromIdx);\r
7427
- const to = particlePos(c.toIdx);\r
7428
- const head = new THREE.Vector3().lerpVectors(from, to, c.t);\r
7429
- \r
7430
- // Only record a tail point when head has moved far enough in world space\r
7431
- const distMoved = c.lastTailPos ? head.distanceTo(c.lastTailPos) : Infinity;\r
7432
- if (distMoved >= CRAWLER_MIN_STEP) {\r
7433
- c.tail.push(head.clone());\r
7434
- if (c.tail.length > CRAWLER_TAIL) c.tail.shift();\r
7435
- c.lastTailPos = head.clone();\r
7436
- }\r
7437
- \r
7438
- // Write tail segments\r
7439
- const tailLen = c.tail.length;\r
7440
- for (let s = 0; s < tailLen - 1 && segIdx < crawlerMaxSegs; s++) {\r
7441
- const p0 = c.tail[s];\r
7442
- const p1 = c.tail[s + 1];\r
7443
- // Alpha: head is brightest, tail fades\r
7444
- const baseAlpha = ((s + 1) / (tailLen - 1));\r
7445
- const alpha = baseAlpha * (0.25 + spectralCentroid * 0.3 + beatEnergy * 0.4);\r
7446
- \r
7447
- crawlerPos[segIdx * 6 + 0] = p0.x; crawlerPos[segIdx * 6 + 1] = p0.y; crawlerPos[segIdx * 6 + 2] = p0.z;\r
7448
- crawlerPos[segIdx * 6 + 3] = p1.x; crawlerPos[segIdx * 6 + 4] = p1.y; crawlerPos[segIdx * 6 + 5] = p1.z;\r
7449
- crawlerAlp[segIdx * 2 + 0] = alpha;\r
7450
- crawlerAlp[segIdx * 2 + 1] = alpha;\r
7451
- segIdx++;\r
7452
- }\r
7453
- }\r
7454
- \r
7455
- crawlerGeo.attributes.position.needsUpdate = true;\r
7456
- crawlerGeo.attributes.alpha.needsUpdate = true;\r
7457
- crawlerGeo.setDrawRange(0, segIdx * 2);\r
7458
- }\r
7459
- \r
7460
- STATE.crawlersEnabled = true;\r
7461
- \r
7462
- // \u2550\u2550\u2550\u2550\u2550\u2550\u2550 NEBULA BACKGROUND \u2550\u2550\u2550\u2550\u2550\u2550\u2550\r
7463
- const nebulaVertexShader = \`\r
7464
- varying vec3 vWorldPosition;\r
7465
- varying vec2 vUv;\r
7466
- void main() {\r
7467
- vUv = uv;\r
7468
- vec4 worldPos = modelMatrix * vec4(position, 1.0);\r
7469
- vWorldPosition = worldPos.xyz;\r
7470
- gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\r
7471
- }\r
7472
- \`;\r
7473
- \r
7474
- const nebulaFragmentShader = \`\r
7475
- uniform float uTime;\r
7476
- uniform vec3 uColor1;\r
7477
- uniform vec3 uColor2;\r
7478
- uniform vec3 uColor3;\r
7479
- uniform float uBass;\r
7480
- uniform float uMid;\r
7481
- uniform float uHigh;\r
7482
- uniform float uBeatEnergy;\r
7483
- uniform float uIntensity;\r
7484
- varying vec3 vWorldPosition;\r
7485
- varying vec2 vUv;\r
7486
- \r
7487
- // Simplex 3D noise\r
7488
- vec4 permute(vec4 x) { return mod(((x*34.0)+1.0)*x, 289.0); }\r
7489
- vec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; }\r
7490
- \r
7491
- float snoise(vec3 v) {\r
7492
- const vec2 C = vec2(1.0/6.0, 1.0/3.0);\r
7493
- const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);\r
7494
- vec3 i = floor(v + dot(v, C.yyy));\r
7495
- vec3 x0 = v - i + dot(i, C.xxx);\r
7496
- vec3 g = step(x0.yzx, x0.xyz);\r
7497
- vec3 l = 1.0 - g;\r
7498
- vec3 i1 = min(g.xyz, l.zxy);\r
7499
- vec3 i2 = max(g.xyz, l.zxy);\r
7500
- vec3 x1 = x0 - i1 + C.xxx;\r
7501
- vec3 x2 = x0 - i2 + C.yyy;\r
7502
- vec3 x3 = x0 - D.yyy;\r
7503
- i = mod(i, 289.0);\r
7504
- vec4 p = permute(permute(permute(i.z + vec4(0.0, i1.z, i2.z, 1.0)) + i.y + vec4(0.0, i1.y, i2.y, 1.0)) + i.x + vec4(0.0, i1.x, i2.x, 1.0));\r
7505
- float n_ = 1.0/7.0;\r
7506
- vec3 ns = n_ * D.wyz - D.xzx;\r
7507
- vec4 j = p - 49.0 * floor(p * ns.z * ns.z);\r
7508
- vec4 x_ = floor(j * ns.z);\r
7509
- vec4 y_ = floor(j - 7.0 * x_);\r
7510
- vec4 x = x_ *ns.x + ns.yyyy;\r
7511
- vec4 y = y_ *ns.x + ns.yyyy;\r
7512
- vec4 h = 1.0 - abs(x) - abs(y);\r
7513
- vec4 b0 = vec4(x.xy, y.xy);\r
7514
- vec4 b1 = vec4(x.zw, y.zw);\r
7515
- vec4 s0 = floor(b0)*2.0 + 1.0;\r
7516
- vec4 s1 = floor(b1)*2.0 + 1.0;\r
7517
- vec4 sh = -step(h, vec4(0.0));\r
7518
- vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy;\r
7519
- vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww;\r
7520
- vec3 p0 = vec3(a0.xy, h.x);\r
7521
- vec3 p1 = vec3(a0.zw, h.y);\r
7522
- vec3 p2 = vec3(a1.xy, h.z);\r
7523
- vec3 p3 = vec3(a1.zw, h.w);\r
7524
- vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2,p2), dot(p3,p3)));\r
7525
- p0 *= norm.x; p1 *= norm.y; p2 *= norm.z; p3 *= norm.w;\r
7526
- vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);\r
7527
- return 42.0 * dot(m*m*m*m, vec4(dot(p0,x0), dot(p1,x1), dot(p2,x2), dot(p3,x3)));\r
7528
- }\r
7529
- \r
7530
- float fbm(vec3 p) {\r
7531
- float value = 0.0;\r
7532
- float amplitude = 0.5;\r
7533
- float frequency = 1.0;\r
7534
- for(int i = 0; i < 3; i++) {\r
7535
- value += amplitude * snoise(p * frequency);\r
7536
- amplitude *= 0.5;\r
7537
- frequency *= 2.0;\r
7538
- }\r
7539
- return value;\r
7540
- }\r
7541
- \r
7542
- void main() {\r
7543
- vec3 dir = normalize(vWorldPosition);\r
7544
- float t = uTime * 0.02;\r
7545
- \r
7546
- // Layered nebula clouds\r
7547
- vec3 p1 = dir * 2.0 + t * 0.5;\r
7548
- vec3 p2 = dir * 4.0 - t * 0.3;\r
7549
- vec3 p3 = dir * 1.5 + vec3(t * 0.2, -t * 0.1, t * 0.15);\r
7550
- \r
7551
- float n1 = fbm(p1) * 0.5 + 0.5;\r
7552
- float n2 = fbm(p2) * 0.5 + 0.5;\r
7553
- float n3 = fbm(p3) * 0.5 + 0.5;\r
7554
- \r
7555
- // Create cloud density\r
7556
- float density = n1 * n2;\r
7557
- density = pow(density, 1.5) * 1.5;\r
7558
- \r
7559
- // Wispy tendrils\r
7560
- float tendrils = pow(n3, 3.0) * 0.8;\r
7561
- \r
7562
- // Color mixing based on field colors\r
7563
- vec3 nebulaColor = mix(uColor1 * 0.3, uColor2 * 0.5, n1);\r
7564
- nebulaColor = mix(nebulaColor, uColor3 * 0.4, n2 * 0.5);\r
7565
- \r
7566
- // Add some complementary deep space colors\r
7567
- vec3 deepSpace = vec3(0.02, 0.01, 0.05);\r
7568
- vec3 starGlow = vec3(0.1, 0.08, 0.15);\r
7569
- \r
7570
- // Audio reactivity\r
7571
- float bassGlow = uBass * 0.3;\r
7572
- float midShimmer = uMid * 0.2 * sin(uTime * 2.0 + n1 * 10.0);\r
7573
- float beatPulse = uBeatEnergy * 0.4;\r
7574
- \r
7575
- // Combine layers\r
7576
- vec3 color = deepSpace;\r
7577
- color += nebulaColor * density * (0.4 + bassGlow + beatPulse);\r
7578
- color += uColor2 * 0.15 * tendrils * (1.0 + uHigh * 0.5);\r
7579
- color += starGlow * pow(n1 * n2, 4.0) * (1.0 + midShimmer);\r
7580
- \r
7581
- // Scattered stars\r
7582
- float stars = pow(snoise(dir * 50.0) * 0.5 + 0.5, 20.0);\r
7583
- stars += pow(snoise(dir * 80.0 + 100.0) * 0.5 + 0.5, 25.0) * 0.5;\r
7584
- color += vec3(1.0, 0.95, 0.9) * stars * (0.5 + uHigh * 0.5);\r
7585
- \r
7586
- // Vignette toward horizon\r
7587
- float horizonFade = 1.0 - pow(abs(dir.y), 0.5) * 0.3;\r
7588
- color *= horizonFade;\r
7589
- \r
7590
- // Apply intensity\r
7591
- color *= uIntensity;\r
7592
- \r
7593
- gl_FragColor = vec4(color, 1.0);\r
7594
- }\r
7595
- \`;\r
7596
- \r
7597
- const nebulaMaterial = new THREE.ShaderMaterial({\r
7598
- vertexShader: nebulaVertexShader,\r
7599
- fragmentShader: nebulaFragmentShader,\r
7600
- uniforms: {\r
7601
- uTime: { value: 0 },\r
7602
- uColor1: { value: new THREE.Color(def.c1) },\r
7603
- uColor2: { value: new THREE.Color(def.c2) },\r
7604
- uColor3: { value: new THREE.Color(def.c3) },\r
7605
- uBass: { value: 0 },\r
7606
- uMid: { value: 0 },\r
7607
- uHigh: { value: 0 },\r
7608
- uBeatEnergy: { value: 0 },\r
7609
- uIntensity: { value: STATE.nebulaIntensity }\r
7610
- },\r
7611
- side: THREE.BackSide,\r
7612
- depthWrite: false\r
7613
- });\r
7614
- \r
7615
- const nebulaSphere = new THREE.Mesh(\r
7616
- new THREE.SphereGeometry(500, 64, 64),\r
7617
- nebulaMaterial\r
7618
- );\r
7619
- scene.add(nebulaSphere);\r
7620
- \r
7621
- // \u2550\u2550\u2550\u2550\u2550\u2550\u2550 POST-PROCESSING COMPOSER \u2550\u2550\u2550\u2550\u2550\u2550\u2550\r
7622
- const composer = new EffectComposer(renderer);\r
7623
- composer.addPass(new RenderPass(scene, camera));\r
7624
- \r
7625
- const afterimagePass = new AfterimagePass();\r
7626
- afterimagePass.uniforms['damp'].value = STATE.trails;\r
7627
- composer.addPass(afterimagePass);\r
7628
- \r
7629
- const bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5, 0.4, 0.85);\r
7630
- bloomPass.threshold = 0.25;\r
7631
- bloomPass.strength = STATE.bloom;\r
7632
- bloomPass.radius = 0.45;\r
7633
- composer.addPass(bloomPass);\r
7634
- \r
7635
- const godRaysPass = new ShaderPass(GodRaysShader);\r
7636
- godRaysPass.enabled = false;\r
7637
- composer.addPass(godRaysPass);\r
7638
- \r
7382
+ }
7383
+
7384
+ // Helper: find nearest neighbour within threshold
7385
+ function nearestNeighbour(fromIdx, excludeIdx) {
7386
+ const origin = particlePos(fromIdx);
7387
+ const threshold = STATE.connectionThreshold * (1.2 + highMid * 0.4);
7388
+ const threshSq = threshold * threshold;
7389
+ let bestDist = Infinity, bestIdx = -1;
7390
+ const check = Math.min(trackedParticles.length, 80); // sample subset for perf
7391
+ const start = Math.floor(Math.random() * (trackedParticles.length - check));
7392
+ for (let k = start; k < start + check; k++) {
7393
+ const ki = k % trackedParticles.length;
7394
+ if (ki === fromIdx || ki === excludeIdx) continue;
7395
+ const d = origin.distanceToSquared(particlePos(ki));
7396
+ if (d < threshSq && d < bestDist) { bestDist = d; bestIdx = ki; }
7397
+ }
7398
+ // fallback: pick random if no neighbour in range
7399
+ return bestIdx === -1 ? Math.floor(Math.random() * trackedParticles.length) : bestIdx;
7400
+ }
7401
+
7402
+ let segIdx = 0;
7403
+
7404
+ for (let i = 0; i < crawlers.length; i++) {
7405
+ const c = crawlers[i];
7406
+
7407
+ // Speed reacts to highMid (attack energy) and beat
7408
+ const spd = c.speed * (1.0 + highMid * 0.8 + beatEnergy * 0.5);
7409
+ c.t += spd;
7410
+
7411
+ // Reached target \u2014 advance to next node
7412
+ if (c.t >= 1.0) {
7413
+ c.t -= 1.0;
7414
+ c.fromIdx = c.toIdx;
7415
+ c.toIdx = nearestNeighbour(c.fromIdx, c.fromIdx);
7416
+ // Occasionally teleport to keep crawlers spread across the field
7417
+ if (Math.random() < 0.04) {
7418
+ c.fromIdx = Math.floor(Math.random() * trackedParticles.length);
7419
+ c.toIdx = nearestNeighbour(c.fromIdx, c.fromIdx);
7420
+ c.tail = [];
7421
+ c.lastTailPos = null;
7422
+ }
7423
+ }
7424
+
7425
+ // Current head position
7426
+ const from = particlePos(c.fromIdx);
7427
+ const to = particlePos(c.toIdx);
7428
+ const head = new THREE.Vector3().lerpVectors(from, to, c.t);
7429
+
7430
+ // Only record a tail point when head has moved far enough in world space
7431
+ const distMoved = c.lastTailPos ? head.distanceTo(c.lastTailPos) : Infinity;
7432
+ if (distMoved >= CRAWLER_MIN_STEP) {
7433
+ c.tail.push(head.clone());
7434
+ if (c.tail.length > CRAWLER_TAIL) c.tail.shift();
7435
+ c.lastTailPos = head.clone();
7436
+ }
7437
+
7438
+ // Write tail segments
7439
+ const tailLen = c.tail.length;
7440
+ for (let s = 0; s < tailLen - 1 && segIdx < crawlerMaxSegs; s++) {
7441
+ const p0 = c.tail[s];
7442
+ const p1 = c.tail[s + 1];
7443
+ // Alpha: head is brightest, tail fades
7444
+ const baseAlpha = ((s + 1) / (tailLen - 1));
7445
+ const alpha = baseAlpha * (0.25 + spectralCentroid * 0.3 + beatEnergy * 0.4);
7446
+
7447
+ crawlerPos[segIdx * 6 + 0] = p0.x; crawlerPos[segIdx * 6 + 1] = p0.y; crawlerPos[segIdx * 6 + 2] = p0.z;
7448
+ crawlerPos[segIdx * 6 + 3] = p1.x; crawlerPos[segIdx * 6 + 4] = p1.y; crawlerPos[segIdx * 6 + 5] = p1.z;
7449
+ crawlerAlp[segIdx * 2 + 0] = alpha;
7450
+ crawlerAlp[segIdx * 2 + 1] = alpha;
7451
+ segIdx++;
7452
+ }
7453
+ }
7454
+
7455
+ crawlerGeo.attributes.position.needsUpdate = true;
7456
+ crawlerGeo.attributes.alpha.needsUpdate = true;
7457
+ crawlerGeo.setDrawRange(0, segIdx * 2);
7458
+ }
7459
+
7460
+ STATE.crawlersEnabled = true;
7461
+
7462
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550 NEBULA BACKGROUND \u2550\u2550\u2550\u2550\u2550\u2550\u2550
7463
+ const nebulaVertexShader = \`
7464
+ varying vec3 vWorldPosition;
7465
+ varying vec2 vUv;
7466
+ void main() {
7467
+ vUv = uv;
7468
+ vec4 worldPos = modelMatrix * vec4(position, 1.0);
7469
+ vWorldPosition = worldPos.xyz;
7470
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
7471
+ }
7472
+ \`;
7473
+
7474
+ const nebulaFragmentShader = \`
7475
+ uniform float uTime;
7476
+ uniform vec3 uColor1;
7477
+ uniform vec3 uColor2;
7478
+ uniform vec3 uColor3;
7479
+ uniform float uBass;
7480
+ uniform float uMid;
7481
+ uniform float uHigh;
7482
+ uniform float uBeatEnergy;
7483
+ uniform float uIntensity;
7484
+ varying vec3 vWorldPosition;
7485
+ varying vec2 vUv;
7486
+
7487
+ // Simplex 3D noise
7488
+ vec4 permute(vec4 x) { return mod(((x*34.0)+1.0)*x, 289.0); }
7489
+ vec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; }
7490
+
7491
+ float snoise(vec3 v) {
7492
+ const vec2 C = vec2(1.0/6.0, 1.0/3.0);
7493
+ const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
7494
+ vec3 i = floor(v + dot(v, C.yyy));
7495
+ vec3 x0 = v - i + dot(i, C.xxx);
7496
+ vec3 g = step(x0.yzx, x0.xyz);
7497
+ vec3 l = 1.0 - g;
7498
+ vec3 i1 = min(g.xyz, l.zxy);
7499
+ vec3 i2 = max(g.xyz, l.zxy);
7500
+ vec3 x1 = x0 - i1 + C.xxx;
7501
+ vec3 x2 = x0 - i2 + C.yyy;
7502
+ vec3 x3 = x0 - D.yyy;
7503
+ i = mod(i, 289.0);
7504
+ vec4 p = permute(permute(permute(i.z + vec4(0.0, i1.z, i2.z, 1.0)) + i.y + vec4(0.0, i1.y, i2.y, 1.0)) + i.x + vec4(0.0, i1.x, i2.x, 1.0));
7505
+ float n_ = 1.0/7.0;
7506
+ vec3 ns = n_ * D.wyz - D.xzx;
7507
+ vec4 j = p - 49.0 * floor(p * ns.z * ns.z);
7508
+ vec4 x_ = floor(j * ns.z);
7509
+ vec4 y_ = floor(j - 7.0 * x_);
7510
+ vec4 x = x_ *ns.x + ns.yyyy;
7511
+ vec4 y = y_ *ns.x + ns.yyyy;
7512
+ vec4 h = 1.0 - abs(x) - abs(y);
7513
+ vec4 b0 = vec4(x.xy, y.xy);
7514
+ vec4 b1 = vec4(x.zw, y.zw);
7515
+ vec4 s0 = floor(b0)*2.0 + 1.0;
7516
+ vec4 s1 = floor(b1)*2.0 + 1.0;
7517
+ vec4 sh = -step(h, vec4(0.0));
7518
+ vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy;
7519
+ vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww;
7520
+ vec3 p0 = vec3(a0.xy, h.x);
7521
+ vec3 p1 = vec3(a0.zw, h.y);
7522
+ vec3 p2 = vec3(a1.xy, h.z);
7523
+ vec3 p3 = vec3(a1.zw, h.w);
7524
+ vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2,p2), dot(p3,p3)));
7525
+ p0 *= norm.x; p1 *= norm.y; p2 *= norm.z; p3 *= norm.w;
7526
+ vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);
7527
+ return 42.0 * dot(m*m*m*m, vec4(dot(p0,x0), dot(p1,x1), dot(p2,x2), dot(p3,x3)));
7528
+ }
7529
+
7530
+ float fbm(vec3 p) {
7531
+ float value = 0.0;
7532
+ float amplitude = 0.5;
7533
+ float frequency = 1.0;
7534
+ for(int i = 0; i < 3; i++) {
7535
+ value += amplitude * snoise(p * frequency);
7536
+ amplitude *= 0.5;
7537
+ frequency *= 2.0;
7538
+ }
7539
+ return value;
7540
+ }
7541
+
7542
+ void main() {
7543
+ vec3 dir = normalize(vWorldPosition);
7544
+ float t = uTime * 0.02;
7545
+
7546
+ // Layered nebula clouds
7547
+ vec3 p1 = dir * 2.0 + t * 0.5;
7548
+ vec3 p2 = dir * 4.0 - t * 0.3;
7549
+ vec3 p3 = dir * 1.5 + vec3(t * 0.2, -t * 0.1, t * 0.15);
7550
+
7551
+ float n1 = fbm(p1) * 0.5 + 0.5;
7552
+ float n2 = fbm(p2) * 0.5 + 0.5;
7553
+ float n3 = fbm(p3) * 0.5 + 0.5;
7554
+
7555
+ // Create cloud density
7556
+ float density = n1 * n2;
7557
+ density = pow(density, 1.5) * 1.5;
7558
+
7559
+ // Wispy tendrils
7560
+ float tendrils = pow(n3, 3.0) * 0.8;
7561
+
7562
+ // Color mixing based on field colors
7563
+ vec3 nebulaColor = mix(uColor1 * 0.3, uColor2 * 0.5, n1);
7564
+ nebulaColor = mix(nebulaColor, uColor3 * 0.4, n2 * 0.5);
7565
+
7566
+ // Add some complementary deep space colors
7567
+ vec3 deepSpace = vec3(0.02, 0.01, 0.05);
7568
+ vec3 starGlow = vec3(0.1, 0.08, 0.15);
7569
+
7570
+ // Audio reactivity
7571
+ float bassGlow = uBass * 0.3;
7572
+ float midShimmer = uMid * 0.2 * sin(uTime * 2.0 + n1 * 10.0);
7573
+ float beatPulse = uBeatEnergy * 0.4;
7574
+
7575
+ // Combine layers
7576
+ vec3 color = deepSpace;
7577
+ color += nebulaColor * density * (0.4 + bassGlow + beatPulse);
7578
+ color += uColor2 * 0.15 * tendrils * (1.0 + uHigh * 0.5);
7579
+ color += starGlow * pow(n1 * n2, 4.0) * (1.0 + midShimmer);
7580
+
7581
+ // Scattered stars
7582
+ float stars = pow(snoise(dir * 50.0) * 0.5 + 0.5, 20.0);
7583
+ stars += pow(snoise(dir * 80.0 + 100.0) * 0.5 + 0.5, 25.0) * 0.5;
7584
+ color += vec3(1.0, 0.95, 0.9) * stars * (0.5 + uHigh * 0.5);
7585
+
7586
+ // Vignette toward horizon
7587
+ float horizonFade = 1.0 - pow(abs(dir.y), 0.5) * 0.3;
7588
+ color *= horizonFade;
7589
+
7590
+ // Apply intensity
7591
+ color *= uIntensity;
7592
+
7593
+ gl_FragColor = vec4(color, 1.0);
7594
+ }
7595
+ \`;
7596
+
7597
+ const nebulaMaterial = new THREE.ShaderMaterial({
7598
+ vertexShader: nebulaVertexShader,
7599
+ fragmentShader: nebulaFragmentShader,
7600
+ uniforms: {
7601
+ uTime: { value: 0 },
7602
+ uColor1: { value: new THREE.Color(def.c1) },
7603
+ uColor2: { value: new THREE.Color(def.c2) },
7604
+ uColor3: { value: new THREE.Color(def.c3) },
7605
+ uBass: { value: 0 },
7606
+ uMid: { value: 0 },
7607
+ uHigh: { value: 0 },
7608
+ uBeatEnergy: { value: 0 },
7609
+ uIntensity: { value: STATE.nebulaIntensity }
7610
+ },
7611
+ side: THREE.BackSide,
7612
+ depthWrite: false
7613
+ });
7614
+
7615
+ const nebulaSphere = new THREE.Mesh(
7616
+ new THREE.SphereGeometry(500, 64, 64),
7617
+ nebulaMaterial
7618
+ );
7619
+ scene.add(nebulaSphere);
7620
+
7621
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550 POST-PROCESSING COMPOSER \u2550\u2550\u2550\u2550\u2550\u2550\u2550
7622
+ const composer = new EffectComposer(renderer);
7623
+ composer.addPass(new RenderPass(scene, camera));
7624
+
7625
+ const afterimagePass = new AfterimagePass();
7626
+ afterimagePass.uniforms['damp'].value = STATE.trails;
7627
+ composer.addPass(afterimagePass);
7628
+
7629
+ const bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5, 0.4, 0.85);
7630
+ bloomPass.threshold = 0.25;
7631
+ bloomPass.strength = STATE.bloom;
7632
+ bloomPass.radius = 0.45;
7633
+ composer.addPass(bloomPass);
7634
+
7635
+ const godRaysPass = new ShaderPass(GodRaysShader);
7636
+ godRaysPass.enabled = false;
7637
+ composer.addPass(godRaysPass);
7638
+
7639
7639
  const lensFlarePass = new ShaderPass(LensFlareShader);
7640
7640
  lensFlarePass.enabled = STATE.lensFlareEnabled && STATE.lensFlareIntensity > 0;
7641
- composer.addPass(lensFlarePass);\r
7642
- \r
7643
- const anamorphicPass = new ShaderPass(AnamorphicShader);\r
7644
- composer.addPass(anamorphicPass);\r
7645
- \r
7641
+ composer.addPass(lensFlarePass);
7642
+
7643
+ const anamorphicPass = new ShaderPass(AnamorphicShader);
7644
+ composer.addPass(anamorphicPass);
7645
+
7646
7646
  const chromaticPass = new ShaderPass(ChromaticShader);
7647
7647
  chromaticPass.enabled = STATE.chromaticAberration > 0;
7648
7648
  composer.addPass(chromaticPass);
7649
- \r
7650
- // Custom DOF pass (radial focus)\r
7651
- const customDOFPass = new ShaderPass(CustomDOFShader);\r
7652
- customDOFPass.uniforms.uFocus.value = STATE.dofFocus;\r
7653
- customDOFPass.uniforms.uFocalLength.value = STATE.dofFocalLength;\r
7654
- customDOFPass.uniforms.uBokehStrength.value = STATE.dofBokehStrength;\r
7655
- customDOFPass.uniforms.uResolution.value.set(window.innerWidth, window.innerHeight);\r
7656
- customDOFPass.enabled = STATE.dofEnabled;\r
7657
- composer.addPass(customDOFPass);\r
7658
- \r
7659
- const vignettePass = new ShaderPass(VignetteShader);\r
7660
- composer.addPass(vignettePass);\r
7661
- \r
7649
+
7650
+ // Custom DOF pass (radial focus)
7651
+ const customDOFPass = new ShaderPass(CustomDOFShader);
7652
+ customDOFPass.uniforms.uFocus.value = STATE.dofFocus;
7653
+ customDOFPass.uniforms.uFocalLength.value = STATE.dofFocalLength;
7654
+ customDOFPass.uniforms.uBokehStrength.value = STATE.dofBokehStrength;
7655
+ customDOFPass.uniforms.uResolution.value.set(window.innerWidth, window.innerHeight);
7656
+ customDOFPass.enabled = STATE.dofEnabled;
7657
+ composer.addPass(customDOFPass);
7658
+
7659
+ const vignettePass = new ShaderPass(VignetteShader);
7660
+ composer.addPass(vignettePass);
7661
+
7662
7662
  const grainPass = new ShaderPass(GrainShader);
7663
7663
  grainPass.enabled = STATE.filmGrain > 0;
7664
7664
  composer.addPass(grainPass);
7665
- \r
7665
+
7666
7666
  const AlphaFromLumaShader = {
7667
7667
  uniforms: { tDiffuse: { value: null } },
7668
7668
  vertexShader: \`varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }\`,
@@ -7676,7 +7676,7 @@ void main() {\r
7676
7676
  };
7677
7677
  composer.addPass(new OutputPass());
7678
7678
  composer.addPass(new ShaderPass(AlphaFromLumaShader));
7679
- \r
7679
+
7680
7680
  // Keep the embedded CSS accents aligned with the kit palette.
7681
7681
  function applyCssTheme(accent) {
7682
7682
  const luminance = accent.r * 0.299 + accent.g * 0.587 + accent.b * 0.114;
@@ -7688,51 +7688,51 @@ void main() {\r
7688
7688
  applyCssTheme(new THREE.Color(def.c1));
7689
7689
 
7690
7690
  // \u2550\u2550\u2550\u2550\u2550\u2550\u2550 UI & EVENTS \u2550\u2550\u2550\u2550\u2550\u2550\u2550
7691
- const overlay = document.getElementById('overlay');\r
7692
- const audioControls = document.getElementById('audio-controls');\r
7693
- const playPauseBtn = document.getElementById('play-pause-btn');\r
7694
- const progressBar = document.getElementById('progress-bar');\r
7695
- const progressContainer = document.getElementById('progress-container');\r
7696
- const timeDisplay = document.getElementById('time-display');\r
7697
- const trackName = document.getElementById('track-name');\r
7698
- const status = document.getElementById('status-bar');\r
7699
- \r
7700
- const formatTime = s => \`\${Math.floor(s / 60)}:\${Math.floor(s % 60).toString().padStart(2, '0')}\`;\r
7701
- \r
7702
- function updateUI() {\r
7703
- if (AUDIO.mode === 'mic') {\r
7704
- playPauseBtn.textContent = '\u{1F399} live';\r
7705
- playPauseBtn.disabled = true;\r
7706
- } else {\r
7707
- playPauseBtn.textContent = AUDIO.isPlaying ? '\u23F8 pause' : '\u25B6 play';\r
7708
- playPauseBtn.classList.toggle('playing', AUDIO.isPlaying);\r
7709
- playPauseBtn.disabled = false;\r
7710
- }\r
7711
- if (AUDIO.mode === 'file' && AUDIO.audioBuffer) {\r
7712
- const cur = AUDIO.getCurrentTime(), dur = AUDIO.getDuration();\r
7713
- progressBar.style.width = (cur / dur * 100) + '%';\r
7714
- timeDisplay.textContent = \`\${formatTime(cur)} / \${formatTime(dur)}\`;\r
7715
- } else {\r
7716
- progressBar.style.width = '0%';\r
7717
- timeDisplay.textContent = AUDIO.mode === 'mic' ? 'LIVE' : '0:00 / 0:00';\r
7718
- }\r
7719
- }\r
7720
- \r
7721
- function togglePlayPause() {\r
7722
- if (AUDIO.mode !== 'file') return;\r
7723
- AUDIO.togglePlayPause();\r
7724
- updateUI();\r
7725
- status.innerHTML = AUDIO.isPlaying ? 'SYSTEM: <span>PLAYING</span>' : 'SYSTEM: <span>PAUSED</span>';\r
7726
- }\r
7727
- \r
7728
- function activate() {\r
7729
- AUDIO.init();\r
7730
- if (AUDIO.ctx.state === 'suspended') AUDIO.ctx.resume();\r
7731
- overlay.style.opacity = 0;\r
7732
- setTimeout(() => overlay.style.display = 'none', 800);\r
7733
- audioControls.classList.add('visible');\r
7734
- }\r
7735
- \r
7691
+ const overlay = document.getElementById('overlay');
7692
+ const audioControls = document.getElementById('audio-controls');
7693
+ const playPauseBtn = document.getElementById('play-pause-btn');
7694
+ const progressBar = document.getElementById('progress-bar');
7695
+ const progressContainer = document.getElementById('progress-container');
7696
+ const timeDisplay = document.getElementById('time-display');
7697
+ const trackName = document.getElementById('track-name');
7698
+ const status = document.getElementById('status-bar');
7699
+
7700
+ const formatTime = s => \`\${Math.floor(s / 60)}:\${Math.floor(s % 60).toString().padStart(2, '0')}\`;
7701
+
7702
+ function updateUI() {
7703
+ if (AUDIO.mode === 'mic') {
7704
+ playPauseBtn.textContent = '\u{1F399} live';
7705
+ playPauseBtn.disabled = true;
7706
+ } else {
7707
+ playPauseBtn.textContent = AUDIO.isPlaying ? '\u23F8 pause' : '\u25B6 play';
7708
+ playPauseBtn.classList.toggle('playing', AUDIO.isPlaying);
7709
+ playPauseBtn.disabled = false;
7710
+ }
7711
+ if (AUDIO.mode === 'file' && AUDIO.audioBuffer) {
7712
+ const cur = AUDIO.getCurrentTime(), dur = AUDIO.getDuration();
7713
+ progressBar.style.width = (cur / dur * 100) + '%';
7714
+ timeDisplay.textContent = \`\${formatTime(cur)} / \${formatTime(dur)}\`;
7715
+ } else {
7716
+ progressBar.style.width = '0%';
7717
+ timeDisplay.textContent = AUDIO.mode === 'mic' ? 'LIVE' : '0:00 / 0:00';
7718
+ }
7719
+ }
7720
+
7721
+ function togglePlayPause() {
7722
+ if (AUDIO.mode !== 'file') return;
7723
+ AUDIO.togglePlayPause();
7724
+ updateUI();
7725
+ status.innerHTML = AUDIO.isPlaying ? 'SYSTEM: <span>PLAYING</span>' : 'SYSTEM: <span>PAUSED</span>';
7726
+ }
7727
+
7728
+ function activate() {
7729
+ AUDIO.init();
7730
+ if (AUDIO.ctx.state === 'suspended') AUDIO.ctx.resume();
7731
+ overlay.style.opacity = 0;
7732
+ setTimeout(() => overlay.style.display = 'none', 800);
7733
+ audioControls.classList.add('visible');
7734
+ }
7735
+
7736
7736
  async function loadTrack(file) {
7737
7737
  activate();
7738
7738
  const buffer = await AUDIO.loadTrack(await file.arrayBuffer(), file.name);
@@ -7748,52 +7748,52 @@ void main() {\r
7748
7748
  });
7749
7749
  parent.postMessage({ type: 'ad-qf-waveform', value: peaks }, '*');
7750
7750
  AUDIO.play();
7751
- trackName.textContent = file.name.length > 25 ? file.name.slice(0, 22) + '...' : file.name;\r
7752
- updateUI();\r
7753
- status.innerHTML = 'SYSTEM: <span>PLAYING</span>';\r
7754
- }\r
7755
- \r
7756
- async function startMic(preferredLabel = null) {\r
7757
- console.log('Global startMic requested:', preferredLabel);\r
7758
- try {\r
7759
- activate();\r
7760
- await AUDIO.startMic(preferredLabel);\r
7761
- trackName.textContent = AUDIO.currentTrackName;\r
7762
- updateUI();\r
7763
- // status update handled in AUDIO.startMic\r
7764
- } catch (e) {\r
7765
- console.error("Global startMic failed:", e);\r
7766
- const status = document.getElementById('status-bar');\r
7767
- status.innerHTML = \`ERROR: <span style="color:#ff6b6b">\${e.message}</span>\`;\r
7768
- }\r
7769
- }\r
7770
- \r
7771
- // Using optional chaining/checks because one input was replaced by a button\r
7772
- const enterBtn = document.getElementById('enter-btn');\r
7773
- if (enterBtn) enterBtn.onclick = () => startMic(null);\r
7774
- \r
7775
- const fileInput = document.getElementById('file-input');\r
7776
- if (fileInput) fileInput.onchange = e => e.target.files[0] && loadTrack(e.target.files[0]);\r
7777
- \r
7778
- document.getElementById('file-input-2').onchange = e => { if (e.target.files[0]) loadTrack(e.target.files[0]); e.target.value = ''; };\r
7779
- document.getElementById('mic-btn-2').onclick = () => startMic(null);\r
7780
- playPauseBtn.onclick = togglePlayPause;\r
7781
- progressContainer.onclick = e => {\r
7782
- if (AUDIO.mode !== 'file' || !AUDIO.audioBuffer) return;\r
7783
- const rect = progressContainer.getBoundingClientRect();\r
7784
- AUDIO.seek((e.clientX - rect.left) / rect.width * AUDIO.getDuration());\r
7785
- };\r
7786
- document.getElementById('screenshot-btn')?.addEventListener('click', () => {\r
7787
- composer.render();\r
7788
- const a = document.createElement('a');\r
7789
- a.download = 'quantum-' + Date.now() + '.png';\r
7790
- a.href = renderer.domElement.toDataURL('image/png');\r
7791
- a.click();\r
7792
- });\r
7793
- document.getElementById('fullscreen-btn')?.addEventListener('click', () => {\r
7794
- document.fullscreenElement ? document.exitFullscreen() : document.documentElement.requestFullscreen();\r
7795
- });\r
7796
- \r
7751
+ trackName.textContent = file.name.length > 25 ? file.name.slice(0, 22) + '...' : file.name;
7752
+ updateUI();
7753
+ status.innerHTML = 'SYSTEM: <span>PLAYING</span>';
7754
+ }
7755
+
7756
+ async function startMic(preferredLabel = null) {
7757
+ console.log('Global startMic requested:', preferredLabel);
7758
+ try {
7759
+ activate();
7760
+ await AUDIO.startMic(preferredLabel);
7761
+ trackName.textContent = AUDIO.currentTrackName;
7762
+ updateUI();
7763
+ // status update handled in AUDIO.startMic
7764
+ } catch (e) {
7765
+ console.error("Global startMic failed:", e);
7766
+ const status = document.getElementById('status-bar');
7767
+ status.innerHTML = \`ERROR: <span style="color:#ff6b6b">\${e.message}</span>\`;
7768
+ }
7769
+ }
7770
+
7771
+ // Using optional chaining/checks because one input was replaced by a button
7772
+ const enterBtn = document.getElementById('enter-btn');
7773
+ if (enterBtn) enterBtn.onclick = () => startMic(null);
7774
+
7775
+ const fileInput = document.getElementById('file-input');
7776
+ if (fileInput) fileInput.onchange = e => e.target.files[0] && loadTrack(e.target.files[0]);
7777
+
7778
+ document.getElementById('file-input-2').onchange = e => { if (e.target.files[0]) loadTrack(e.target.files[0]); e.target.value = ''; };
7779
+ document.getElementById('mic-btn-2').onclick = () => startMic(null);
7780
+ playPauseBtn.onclick = togglePlayPause;
7781
+ progressContainer.onclick = e => {
7782
+ if (AUDIO.mode !== 'file' || !AUDIO.audioBuffer) return;
7783
+ const rect = progressContainer.getBoundingClientRect();
7784
+ AUDIO.seek((e.clientX - rect.left) / rect.width * AUDIO.getDuration());
7785
+ };
7786
+ document.getElementById('screenshot-btn')?.addEventListener('click', () => {
7787
+ composer.render();
7788
+ const a = document.createElement('a');
7789
+ a.download = 'quantum-' + Date.now() + '.png';
7790
+ a.href = renderer.domElement.toDataURL('image/png');
7791
+ a.click();
7792
+ });
7793
+ document.getElementById('fullscreen-btn')?.addEventListener('click', () => {
7794
+ document.fullscreenElement ? document.exitFullscreen() : document.documentElement.requestFullscreen();
7795
+ });
7796
+
7797
7797
  // \u2550\u2550\u2550\u2550\u2550\u2550\u2550 ANIMATION LOOP \u2550\u2550\u2550\u2550\u2550\u2550\u2550
7798
7798
  let renderVisible = true;
7799
7799
  let connectionElapsed = 1 / 20;
@@ -7829,60 +7829,60 @@ void main() {\r
7829
7829
  const dt = lastRenderedAt < 0 ? 0 : Math.min((now - lastRenderedAt) / 1000, 0.05);
7830
7830
  lastRenderedAt = now;
7831
7831
  elapsed += dt;
7832
- \r
7833
- material.uniforms.uTime.value += dt * STATE.timeScale;\r
7834
- grainPass.uniforms.uTime.value = elapsed;\r
7835
- lensFlarePass.uniforms.uTime.value = elapsed;\r
7836
- \r
7837
- material.uniforms.uZoomFactor.value = camera.position.length() / CONFIG.defaultZoom;\r
7838
- material.uniforms.uMousePos.value.lerp(mouseWorldPos, 0.1);\r
7839
- material.uniforms.uMouseVelocity.value.lerp(mouseVelocity, 0.08);\r
7840
- \r
7832
+
7833
+ material.uniforms.uTime.value += dt * STATE.timeScale;
7834
+ grainPass.uniforms.uTime.value = elapsed;
7835
+ lensFlarePass.uniforms.uTime.value = elapsed;
7836
+
7837
+ material.uniforms.uZoomFactor.value = camera.position.length() / CONFIG.defaultZoom;
7838
+ material.uniforms.uMousePos.value.lerp(mouseWorldPos, 0.1);
7839
+ material.uniforms.uMouseVelocity.value.lerp(mouseVelocity, 0.08);
7840
+
7841
7841
  AUDIO.update();
7842
- \r
7843
- const s = STATE.sensitivity, lf = 0.16;\r
7844
- material.uniforms.uSubBass.value = THREE.MathUtils.lerp(material.uniforms.uSubBass.value, AUDIO.gatedBands.subBass * s, lf);\r
7845
- material.uniforms.uBass.value = THREE.MathUtils.lerp(material.uniforms.uBass.value, AUDIO.gatedBands.bass * s, lf);\r
7846
- material.uniforms.uLowMid.value = THREE.MathUtils.lerp(material.uniforms.uLowMid.value, AUDIO.gatedBands.lowMid * s, lf);\r
7847
- material.uniforms.uMid.value = THREE.MathUtils.lerp(material.uniforms.uMid.value, AUDIO.gatedBands.mid * s, lf);\r
7848
- material.uniforms.uHighMid.value = THREE.MathUtils.lerp(material.uniforms.uHighMid.value, AUDIO.gatedBands.highMid * s, lf);\r
7849
- material.uniforms.uHigh.value = THREE.MathUtils.lerp(material.uniforms.uHigh.value, AUDIO.gatedBands.high * s, lf);\r
7850
- material.uniforms.uUltraHigh.value = THREE.MathUtils.lerp(material.uniforms.uUltraHigh.value, AUDIO.gatedBands.ultraHigh * s, lf);\r
7851
- material.uniforms.uBeatEnergy.value = THREE.MathUtils.lerp(material.uniforms.uBeatEnergy.value, AUDIO.beatEnergy, 0.22);\r
7852
- material.uniforms.uSpectralCentroid.value = THREE.MathUtils.lerp(material.uniforms.uSpectralCentroid.value, AUDIO.spectralCentroid, lf);\r
7853
- material.uniforms.uSpectralFlux.value = THREE.MathUtils.lerp(material.uniforms.uSpectralFlux.value, AUDIO.spectralFlux * s, lf);\r
7854
- \r
7855
- // === ENHANCEMENT: Update new uniforms ===\r
7856
- material.uniforms.uOnsetEnergy.value = THREE.MathUtils.lerp(material.uniforms.uOnsetEnergy.value, AUDIO.onsetEnergy, 0.25);\r
7857
- material.uniforms.uTerrainMode.value = FIELD_TYPES[STATE.field]?.terrain ? 1.0 : 0.0;\r
7858
- material.uniforms.uTerrainHeight.value = STATE.waveformTerrainHeight;\r
7859
- \r
7860
- // Update secondary particles\r
7861
- secondaryPoints.visible = STATE.secondaryEnabled;\r
7862
- secondaryMaterial.uniforms.uTime.value = material.uniforms.uTime.value;\r
7863
- secondaryMaterial.uniforms.uSubBass.value = material.uniforms.uSubBass.value;\r
7864
- secondaryMaterial.uniforms.uBass.value = material.uniforms.uBass.value;\r
7865
- secondaryMaterial.uniforms.uLowMid.value = material.uniforms.uLowMid.value;\r
7866
- secondaryMaterial.uniforms.uMid.value = material.uniforms.uMid.value;\r
7867
- secondaryMaterial.uniforms.uHighMid.value = material.uniforms.uHighMid.value;\r
7868
- secondaryMaterial.uniforms.uHigh.value = material.uniforms.uHigh.value;\r
7869
- secondaryMaterial.uniforms.uUltraHigh.value = material.uniforms.uUltraHigh.value;\r
7870
- secondaryMaterial.uniforms.uBeatEnergy.value = material.uniforms.uBeatEnergy.value;\r
7871
- secondaryMaterial.uniforms.uSpectralCentroid.value = material.uniforms.uSpectralCentroid.value;\r
7872
- secondaryMaterial.uniforms.uSpectralFlux.value = material.uniforms.uSpectralFlux.value;\r
7873
- secondaryMaterial.uniforms.uOnsetEnergy.value = material.uniforms.uOnsetEnergy.value;\r
7874
- secondaryMaterial.uniforms.uVortexStrength.value = STATE.vortexStrength;\r
7875
- \r
7876
- // Update nebula background\r
7877
- nebulaSphere.visible = STATE.nebulaEnabled;\r
7878
- if (STATE.nebulaEnabled) {\r
7879
- nebulaMaterial.uniforms.uTime.value = material.uniforms.uTime.value;\r
7880
- nebulaMaterial.uniforms.uBass.value = material.uniforms.uBass.value;\r
7881
- nebulaMaterial.uniforms.uMid.value = material.uniforms.uMid.value;\r
7882
- nebulaMaterial.uniforms.uHigh.value = material.uniforms.uHigh.value;\r
7883
- nebulaMaterial.uniforms.uBeatEnergy.value = material.uniforms.uBeatEnergy.value;\r
7884
- }\r
7885
- \r
7842
+
7843
+ const s = STATE.sensitivity, lf = 0.16;
7844
+ material.uniforms.uSubBass.value = THREE.MathUtils.lerp(material.uniforms.uSubBass.value, AUDIO.gatedBands.subBass * s, lf);
7845
+ material.uniforms.uBass.value = THREE.MathUtils.lerp(material.uniforms.uBass.value, AUDIO.gatedBands.bass * s, lf);
7846
+ material.uniforms.uLowMid.value = THREE.MathUtils.lerp(material.uniforms.uLowMid.value, AUDIO.gatedBands.lowMid * s, lf);
7847
+ material.uniforms.uMid.value = THREE.MathUtils.lerp(material.uniforms.uMid.value, AUDIO.gatedBands.mid * s, lf);
7848
+ material.uniforms.uHighMid.value = THREE.MathUtils.lerp(material.uniforms.uHighMid.value, AUDIO.gatedBands.highMid * s, lf);
7849
+ material.uniforms.uHigh.value = THREE.MathUtils.lerp(material.uniforms.uHigh.value, AUDIO.gatedBands.high * s, lf);
7850
+ material.uniforms.uUltraHigh.value = THREE.MathUtils.lerp(material.uniforms.uUltraHigh.value, AUDIO.gatedBands.ultraHigh * s, lf);
7851
+ material.uniforms.uBeatEnergy.value = THREE.MathUtils.lerp(material.uniforms.uBeatEnergy.value, AUDIO.beatEnergy, 0.22);
7852
+ material.uniforms.uSpectralCentroid.value = THREE.MathUtils.lerp(material.uniforms.uSpectralCentroid.value, AUDIO.spectralCentroid, lf);
7853
+ material.uniforms.uSpectralFlux.value = THREE.MathUtils.lerp(material.uniforms.uSpectralFlux.value, AUDIO.spectralFlux * s, lf);
7854
+
7855
+ // === ENHANCEMENT: Update new uniforms ===
7856
+ material.uniforms.uOnsetEnergy.value = THREE.MathUtils.lerp(material.uniforms.uOnsetEnergy.value, AUDIO.onsetEnergy, 0.25);
7857
+ material.uniforms.uTerrainMode.value = FIELD_TYPES[STATE.field]?.terrain ? 1.0 : 0.0;
7858
+ material.uniforms.uTerrainHeight.value = STATE.waveformTerrainHeight;
7859
+
7860
+ // Update secondary particles
7861
+ secondaryPoints.visible = STATE.secondaryEnabled;
7862
+ secondaryMaterial.uniforms.uTime.value = material.uniforms.uTime.value;
7863
+ secondaryMaterial.uniforms.uSubBass.value = material.uniforms.uSubBass.value;
7864
+ secondaryMaterial.uniforms.uBass.value = material.uniforms.uBass.value;
7865
+ secondaryMaterial.uniforms.uLowMid.value = material.uniforms.uLowMid.value;
7866
+ secondaryMaterial.uniforms.uMid.value = material.uniforms.uMid.value;
7867
+ secondaryMaterial.uniforms.uHighMid.value = material.uniforms.uHighMid.value;
7868
+ secondaryMaterial.uniforms.uHigh.value = material.uniforms.uHigh.value;
7869
+ secondaryMaterial.uniforms.uUltraHigh.value = material.uniforms.uUltraHigh.value;
7870
+ secondaryMaterial.uniforms.uBeatEnergy.value = material.uniforms.uBeatEnergy.value;
7871
+ secondaryMaterial.uniforms.uSpectralCentroid.value = material.uniforms.uSpectralCentroid.value;
7872
+ secondaryMaterial.uniforms.uSpectralFlux.value = material.uniforms.uSpectralFlux.value;
7873
+ secondaryMaterial.uniforms.uOnsetEnergy.value = material.uniforms.uOnsetEnergy.value;
7874
+ secondaryMaterial.uniforms.uVortexStrength.value = STATE.vortexStrength;
7875
+
7876
+ // Update nebula background
7877
+ nebulaSphere.visible = STATE.nebulaEnabled;
7878
+ if (STATE.nebulaEnabled) {
7879
+ nebulaMaterial.uniforms.uTime.value = material.uniforms.uTime.value;
7880
+ nebulaMaterial.uniforms.uBass.value = material.uniforms.uBass.value;
7881
+ nebulaMaterial.uniforms.uMid.value = material.uniforms.uMid.value;
7882
+ nebulaMaterial.uniforms.uHigh.value = material.uniforms.uHigh.value;
7883
+ nebulaMaterial.uniforms.uBeatEnergy.value = material.uniforms.uBeatEnergy.value;
7884
+ }
7885
+
7886
7886
  // Update particle connections
7887
7887
  connectionMesh.visible = STATE.connectionsEnabled;
7888
7888
  if (STATE.connectionsEnabled) {
@@ -7917,66 +7917,66 @@ void main() {\r
7917
7917
  idleConnectionElapsed = 2;
7918
7918
  lastConnectionState = null;
7919
7919
  }
7920
- \r
7921
- // Update crawlers\r
7922
- crawlerMesh.visible = STATE.crawlersEnabled && STATE.connectionsEnabled && AUDIO.active && (AUDIO.isPlaying || AUDIO.mode === 'mic');\r
7923
- if (crawlerMesh.visible) {\r
7924
- updateCrawlers(\r
7925
- dt,\r
7926
- material.uniforms.uHighMid.value,\r
7927
- material.uniforms.uBeatEnergy.value,\r
7928
- material.uniforms.uSpectralCentroid.value\r
7929
- );\r
7930
- crawlerMaterial.uniforms.uColor.value.lerp(\r
7931
- new THREE.Color().lerpColors(\r
7932
- new THREE.Color(FIELD_TYPES[STATE.field]?.c1 || '#ffffff'),\r
7933
- new THREE.Color(FIELD_TYPES[STATE.field]?.c3 || '#ffffff'),\r
7934
- material.uniforms.uSpectralCentroid.value\r
7935
- ), 0.05\r
7936
- );\r
7937
- crawlerMaterial.uniforms.uBeatEnergy.value = material.uniforms.uBeatEnergy.value;\r
7938
- }\r
7939
- \r
7940
- // Update post-processing\r
7941
- chromaticPass.uniforms.uBeatEnergy.value = material.uniforms.uBeatEnergy.value;\r
7942
- godRaysPass.uniforms.uBeatEnergy.value = material.uniforms.uBeatEnergy.value;\r
7943
- lensFlarePass.uniforms.uBeatEnergy.value = material.uniforms.uBeatEnergy.value;\r
7944
- vignettePass.uniforms.uBeatEnergy.value = material.uniforms.uBeatEnergy.value;\r
7945
- customDOFPass.uniforms.uBeatEnergy.value = material.uniforms.uBeatEnergy.value;\r
7946
- \r
7947
- \r
7948
- \r
7949
- // Update trails\r
7950
- trailSystem.update(elapsed, {\r
7951
- bass: material.uniforms.uBass.value,\r
7952
- mid: material.uniforms.uMid.value,\r
7953
- high: material.uniforms.uHigh.value,\r
7954
- beat: material.uniforms.uBeatEnergy.value\r
7955
- }, material.uniforms.uColor1.value);\r
7956
- \r
7957
- // === ENHANCEMENT: Adaptive Quality System ===\r
7958
- QUALITY_SYSTEM.update(dt * 1000);\r
7959
- \r
7960
- // === ENHANCEMENT: Audio-Reactive Camera ===\r
7961
- AUDIO_CAMERA.update(dt * 1000);\r
7962
- \r
7963
- // Camera shake\r
7964
- if (STATE.cameraShake > 0 && AUDIO.beatEnergy > 0.1) {\r
7965
- const shake = AUDIO.beatEnergy * STATE.cameraShake;\r
7966
- cameraShakeOffset.set((Math.random() - 0.5) * shake * 0.6, (Math.random() - 0.5) * shake * 0.4, (Math.random() - 0.5) * shake * 0.25);\r
7967
- camera.position.copy(baseCameraPos).add(cameraShakeOffset);\r
7968
- }\r
7969
- \r
7970
- controls.autoRotateSpeed = 0.35 + material.uniforms.uMid.value * 2.0 + material.uniforms.uBeatEnergy.value * 2.0;\r
7920
+
7921
+ // Update crawlers
7922
+ crawlerMesh.visible = STATE.crawlersEnabled && STATE.connectionsEnabled && AUDIO.active && (AUDIO.isPlaying || AUDIO.mode === 'mic');
7923
+ if (crawlerMesh.visible) {
7924
+ updateCrawlers(
7925
+ dt,
7926
+ material.uniforms.uHighMid.value,
7927
+ material.uniforms.uBeatEnergy.value,
7928
+ material.uniforms.uSpectralCentroid.value
7929
+ );
7930
+ crawlerMaterial.uniforms.uColor.value.lerp(
7931
+ new THREE.Color().lerpColors(
7932
+ new THREE.Color(FIELD_TYPES[STATE.field]?.c1 || '#ffffff'),
7933
+ new THREE.Color(FIELD_TYPES[STATE.field]?.c3 || '#ffffff'),
7934
+ material.uniforms.uSpectralCentroid.value
7935
+ ), 0.05
7936
+ );
7937
+ crawlerMaterial.uniforms.uBeatEnergy.value = material.uniforms.uBeatEnergy.value;
7938
+ }
7939
+
7940
+ // Update post-processing
7941
+ chromaticPass.uniforms.uBeatEnergy.value = material.uniforms.uBeatEnergy.value;
7942
+ godRaysPass.uniforms.uBeatEnergy.value = material.uniforms.uBeatEnergy.value;
7943
+ lensFlarePass.uniforms.uBeatEnergy.value = material.uniforms.uBeatEnergy.value;
7944
+ vignettePass.uniforms.uBeatEnergy.value = material.uniforms.uBeatEnergy.value;
7945
+ customDOFPass.uniforms.uBeatEnergy.value = material.uniforms.uBeatEnergy.value;
7946
+
7947
+
7948
+
7949
+ // Update trails
7950
+ trailSystem.update(elapsed, {
7951
+ bass: material.uniforms.uBass.value,
7952
+ mid: material.uniforms.uMid.value,
7953
+ high: material.uniforms.uHigh.value,
7954
+ beat: material.uniforms.uBeatEnergy.value
7955
+ }, material.uniforms.uColor1.value);
7956
+
7957
+ // === ENHANCEMENT: Adaptive Quality System ===
7958
+ QUALITY_SYSTEM.update(dt * 1000);
7959
+
7960
+ // === ENHANCEMENT: Audio-Reactive Camera ===
7961
+ AUDIO_CAMERA.update(dt * 1000);
7962
+
7963
+ // Camera shake
7964
+ if (STATE.cameraShake > 0 && AUDIO.beatEnergy > 0.1) {
7965
+ const shake = AUDIO.beatEnergy * STATE.cameraShake;
7966
+ cameraShakeOffset.set((Math.random() - 0.5) * shake * 0.6, (Math.random() - 0.5) * shake * 0.4, (Math.random() - 0.5) * shake * 0.25);
7967
+ camera.position.copy(baseCameraPos).add(cameraShakeOffset);
7968
+ }
7969
+
7970
+ controls.autoRotateSpeed = 0.35 + material.uniforms.uMid.value * 2.0 + material.uniforms.uBeatEnergy.value * 2.0;
7971
7971
  bloomPass.strength = STATE.bloom + (AUDIO.gatedBands.bass + AUDIO.gatedBands.mid + AUDIO.beatEnergy) / 6;
7972
7972
  bloomPass.enabled = bloomPass.strength > 0;
7973
7973
  anamorphicPass.uniforms.uIntensity.value = STATE.anamorphicStretch + AUDIO.gatedBands.bass * 0.4;
7974
7974
  anamorphicPass.enabled = anamorphicPass.uniforms.uIntensity.value > 0;
7975
- \r
7976
- controls.update();\r
7977
- composer.render();\r
7978
- }\r
7979
- \r
7975
+
7976
+ controls.update();
7977
+ composer.render();
7978
+ }
7979
+
7980
7980
  window.addEventListener('resize', () => {
7981
7981
  camera.aspect = window.innerWidth / window.innerHeight;
7982
7982
  camera.updateProjectionMatrix();
@@ -7984,14 +7984,14 @@ void main() {\r
7984
7984
  secondaryMaterial.uniforms.uViewportAspect.value = material.uniforms.uViewportAspect.value;
7985
7985
  material.uniforms.uViewportHalfHeight.value = Math.tan(camera.fov * Math.PI / 360) * CONFIG.defaultZoom;
7986
7986
  secondaryMaterial.uniforms.uViewportHalfHeight.value = material.uniforms.uViewportHalfHeight.value;
7987
- renderer.setSize(window.innerWidth, window.innerHeight);\r
7987
+ renderer.setSize(window.innerWidth, window.innerHeight);
7988
7988
  composer.setSize(window.innerWidth, window.innerHeight);
7989
7989
  STATE.pixelRatio = fitPixelRatio();
7990
7990
  QUALITY_SYSTEM.applyQuality();
7991
7991
  customDOFPass.uniforms.uResolution.value.set(window.innerWidth, window.innerHeight);
7992
- baseCameraPos.copy(camera.position);\r
7993
- });\r
7994
- \r
7992
+ baseCameraPos.copy(camera.position);
7993
+ });
7994
+
7995
7995
  let pausedFromKit = false;
7996
7996
  let requestedTimeScale = STATE.timeScale;
7997
7997
  const sideEffects = {
@@ -8074,13 +8074,13 @@ void main() {\r
8074
8074
  }
8075
8075
  }, 250);
8076
8076
  parent.postMessage({ type: 'ad-qf-ready' }, '*');
8077
- \r
8077
+
8078
8078
  QUALITY_SYSTEM.applyQuality();
8079
8079
  startFrames();
8080
- <\/script>\r
8081
- </body>\r
8082
- \r
8083
- </html>\r
8080
+ <\/script>
8081
+ </body>
8082
+
8083
+ </html>
8084
8084
  `;
8085
8085
 
8086
8086
  // src/components/artwork/QuantumField/QuantumFieldExperience.tsx
@@ -8228,7 +8228,7 @@ var TransportControls = /* @__PURE__ */ memo(function TransportControls2({ send,
8228
8228
  ] })
8229
8229
  ] });
8230
8230
  });
8231
- function QuantumFieldExperience({ className = "", particleBudget, ...props }) {
8231
+ function QuantumBackground({ className = "", particleBudget, ...props }) {
8232
8232
  const frame = useRef28(null);
8233
8233
  const viewport = useRef28(null);
8234
8234
  const theme = useThemePalette();
@@ -8292,19 +8292,20 @@ function QuantumFieldExperience({ className = "", particleBudget, ...props }) {
8292
8292
  send("palette", [theme.primary, theme.secondary]);
8293
8293
  }, [ready, theme.primary, theme.secondary]);
8294
8294
  return /* @__PURE__ */ jsxs39("div", { "data-ad-component": "QuantumFieldExperience", className: `ad-quantum-field-experience ${className}`.trim(), ...props, children: [
8295
- /* @__PURE__ */ jsx48("div", { className: "ad-qf-viewport", ref: viewport, children: mounted && /* @__PURE__ */ jsx48("iframe", { ref: frame, title: "Quantum Field visualizer", srcDoc: reference_default, allow: "microphone", "data-particle-budget": normalizedBudget }) }),
8295
+ /* @__PURE__ */ jsx48("div", { className: "ad-qf-viewport", ref: viewport, children: mounted && /* @__PURE__ */ jsx48("iframe", { ref: frame, title: "Quantum Background visualizer", srcDoc: reference_default, allow: "microphone", "data-particle-budget": normalizedBudget }) }),
8296
8296
  /* @__PURE__ */ jsxs39("div", { className: "ad-qf-controls", children: [
8297
8297
  /* @__PURE__ */ jsxs39("div", { className: "ad-qf-toolbar", children: [
8298
8298
  /* @__PURE__ */ jsx48(TransportControls, { send, viewport, receive: transportReceiver }),
8299
8299
  /* @__PURE__ */ jsx48("div", { className: "ad-qf-setting-grid ad-qf-essentials", children: essentialControls.map(renderControl) })
8300
8300
  ] }),
8301
8301
  /* @__PURE__ */ jsxs39(CollapsibleSection, { title: "\u0422\u043E\u043D\u043A\u0430\u044F \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0430", description: "\u0421\u0440\u0435\u0434\u0430, \u043A\u0430\u043C\u0435\u0440\u0430, \u044D\u0444\u0444\u0435\u043A\u0442\u044B \u0438 \u0440\u0435\u0430\u043A\u0446\u0438\u044F \u043D\u0430 \u0437\u0432\u0443\u043A", icon: "settings", defaultOpen: false, children: [
8302
- /* @__PURE__ */ jsx48(Tabs, { label: "\u0422\u043E\u043D\u043A\u0430\u044F \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0430 Quantum Field", items: detailedGroups.map(({ id, label }) => ({ value: id, label })), value: tab, onValueChange: setTab }),
8302
+ /* @__PURE__ */ jsx48(Tabs, { label: "\u0422\u043E\u043D\u043A\u0430\u044F \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0430 Quantum Background", items: detailedGroups.map(({ id, label }) => ({ value: id, label })), value: tab, onValueChange: setTab }),
8303
8303
  /* @__PURE__ */ jsx48("div", { className: "ad-qf-control-panel", role: "tabpanel", "aria-label": group.label, children: /* @__PURE__ */ jsx48("div", { className: "ad-qf-setting-grid", children: group.controls.filter(({ key }) => !enabledBy[key] || settings[enabledBy[key]] === true).map(renderControl) }) })
8304
8304
  ] })
8305
8305
  ] })
8306
8306
  ] });
8307
8307
  }
8308
+ var QuantumFieldExperience = QuantumBackground;
8308
8309
 
8309
8310
  // src/components/artwork/QuantumField/audio.ts
8310
8311
  function createQuantumFieldAudio() {
@@ -9570,6 +9571,7 @@ export {
9570
9571
  Planet,
9571
9572
  Popover,
9572
9573
  ProgressBar,
9574
+ QuantumBackground,
9573
9575
  QuantumField,
9574
9576
  QuantumFieldExperience,
9575
9577
  Reveal,