@neo-ui-kit/react 1.1.0 → 1.2.0

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.
package/README.md CHANGED
@@ -25,7 +25,7 @@ import "@neo-ui-kit/react/styles.css";
25
25
  To test repository changes before publishing, run `npm run package` from the repository root and install the resulting archive in a separate application:
26
26
 
27
27
  ```bash
28
- npm install /path/to/adui/release/neo-ui-kit-react-1.1.0.tgz
28
+ npm install /path/to/adui/release/neo-ui-kit-react-1.2.0.tgz
29
29
  ```
30
30
 
31
31
  Maintainers publish new versions from `Pro100Dimka/adui` using the `release.yml` GitHub Actions workflow, a matching `v<version>` tag, and the `npm-publication` environment. npm trusts its OIDC identity; no `NPM_TOKEN` is required. The workflow checks the project and packed consumer before publishing the tested archive. Published `1.0.0` cannot be overwritten: code and README changes in npm require a new package version. See the [repository release instructions](https://github.com/Pro100Dimka/adui#npm-пакет).
@@ -182,6 +182,11 @@ import { ThemeProvider, useMotion } from "@neo-ui-kit/react/core";
182
182
 
183
183
  Canvas-rendered volume knob. Drag near the edge to rotate, drag from the center to move linearly, click the outer scale to jump. Wheel and arrow keys change the value, Shift gives a fine step, PageUp/PageDown change by 10, Home/End set 0/100, Escape cancels a drag, double-click resets. `onValueChange` fires while adjusting, `onValueCommit` after the gesture. `readOnly` turns it into a gauge.
184
184
 
185
+ ## 1.2.0 release notes
186
+
187
+ - `QuantumBackground` takes `threeUrl`, the base address of a three.js 0.182 copy (the folder with `build/` and `examples/jsm/`), so the scene can work offline and without a third-party CDN. A relative address keeps the scene's import map — and its CSP hash — the same wherever the app runs. The default stays `quantumBackgroundThreeUrl` (unpkg).
188
+ - The scene no longer loads web fonts from Google Fonts: it makes no requests beyond three.js.
189
+
185
190
  ## 1.1.0 release notes
186
191
 
187
192
  - **`QuantumBackground` is configured through props.** Every scene setting (`field`, `sensitivity`, `density`, `bloom`, …) is an optional prop with defaults in `quantumBackgroundDefaults`; `quantumBackgroundControls` describes them for building a settings panel. **The component no longer draws its own control panel** — build one next to it (the documentation page shows how) and drive audio through the ref (`QuantumBackgroundHandle`) with `onTransportChange` / `onWaveformChange`.
@@ -247,6 +252,8 @@ import "@neo-ui-kit/react/styles.css";
247
252
 
248
253
  Settings (`QuantumBackgroundSettings`): `field` (one of `quantumBackgroundFields`), `sensitivity`, `density`, `timeScale`, `paused`, `interactive`, nebula, connection, crawler, particle-trail, lens-flare, depth-of-field, camera-shake, bloom, trails, chromatic, anamorphic, god-ray, grain, vortex, pulse, bass-gate, onset, audio-camera, terrain-height and adaptive-quality values. Unset props use `quantumBackgroundDefaults`; only props that change are sent to the scene. `quantumBackgroundControls` lists each setting's group, label, range, step and the switch it depends on, for building your own settings panel next to the component. `particleCount` sets how many particles are drawn across both layers (0 to `quantumBackgroundMaxParticles`, 85 000); leave it unset to follow `density`. Adaptive quality may draw fewer, so turn `adaptiveQualityEnabled` off when the exact count matters. `particleBudget` is an upper cap on top of either. `fieldSize` is the share of the scene the field spans (0.78 by default, 1 reaches the edges). `fullscreen` makes the scene cover the whole viewport above the page, and the field then runs edge to edge (`quantumBackgroundFullscreenFieldSize`, 1.1) unless `fieldSize` is set (in the browser's top layer, so no ancestor's `overflow`, `transform` or `z-index` clips it, and the iframe keeps running); Escape calls `onFullscreenChange(false)`, so keep it controlled. `children` are laid over the scene, e.g. a close button while full screen. This differs from `ref.toggleFullscreen()`, which uses the browser's Fullscreen API and needs a click.
249
254
 
255
+ `threeUrl` points the scene at your own copy of three.js 0.182 instead of unpkg (for example `threeUrl="./vendor/three/"` with `build/three.module.js` and `examples/jsm/` served there); with it the scene makes no network requests at all. Only the scene's import map changes, so a strict CSP pins the same script hashes everywhere.
256
+
250
257
  `spectrum` feeds sound the scene cannot hear itself — a native audio engine, a WebRTC call, a server stream: band levels 0–1 from low to high frequencies, log-spaced across `spectrumRange` (default `[40, 12000]` Hz), any band count. Pass a new array whenever the levels change (10–60 times a second); bands, beats, reaction lines and flashes follow it exactly as they follow a playing song. A file or microphone started from the ref takes over while it plays; `spectrum={undefined}` returns the scene to silence.
251
258
 
252
259
  ```tsx
@@ -1,4 +1,6 @@
1
1
  import { type HTMLAttributes, type ReactNode } from "react";
2
+ /** Where the scene loads three.js from unless `threeUrl` says otherwise: the version it is built and tested with. */
3
+ export declare const quantumBackgroundThreeUrl = "https://unpkg.com/three@0.182.0/";
2
4
  export declare const quantumBackgroundFields: readonly ["Photon (EM)", "Gluon (Strong)", "Higgs (Mass)", "Gravity", "Dark Energy", "Neutrino", "Waveform Terrain"];
3
5
  export type QuantumBackgroundField = typeof quantumBackgroundFields[number];
4
6
  /** Every visual and audio-reaction setting of the scene. All of them are optional props of `QuantumBackground`. */
@@ -102,6 +104,13 @@ export type QuantumBackgroundProps = Partial<QuantumBackgroundSettings> & Omit<H
102
104
  particleCount?: number;
103
105
  /** Maximum number of visible particles across both field layers. Adaptive quality may draw fewer. */
104
106
  particleBudget?: number;
107
+ /**
108
+ * Base address of a three.js 0.182 copy — the folder holding `build/three.module.js` and
109
+ * `examples/jsm/` — so the scene works offline and without a third-party CDN. A relative address
110
+ * resolves against the page and keeps the scene's import map (and its CSP hash) the same wherever
111
+ * the app is installed. Default: `quantumBackgroundThreeUrl` (unpkg).
112
+ */
113
+ threeUrl?: string;
105
114
  /**
106
115
  * Sound the scene cannot hear itself — a native audio engine, a WebRTC call, a server stream: band
107
116
  * levels 0–1 from low to high frequencies, log-spaced across `spectrumRange`. Any band count works;
@@ -132,6 +141,13 @@ export declare const QuantumBackground: import("react").ForwardRefExoticComponen
132
141
  particleCount?: number;
133
142
  /** Maximum number of visible particles across both field layers. Adaptive quality may draw fewer. */
134
143
  particleBudget?: number;
144
+ /**
145
+ * Base address of a three.js 0.182 copy — the folder holding `build/three.module.js` and
146
+ * `examples/jsm/` — so the scene works offline and without a third-party CDN. A relative address
147
+ * resolves against the page and keeps the scene's import map (and its CSP hash) the same wherever
148
+ * the app is installed. Default: `quantumBackgroundThreeUrl` (unpkg).
149
+ */
150
+ threeUrl?: string;
135
151
  /**
136
152
  * Sound the scene cannot hear itself — a native audio engine, a WebRTC call, a server stream: band
137
153
  * levels 0–1 from low to high frequencies, log-spaced across `spectrumRange`. Any band count works;
@@ -162,6 +178,13 @@ export declare const QuantumFieldExperience: import("react").ForwardRefExoticCom
162
178
  particleCount?: number;
163
179
  /** Maximum number of visible particles across both field layers. Adaptive quality may draw fewer. */
164
180
  particleBudget?: number;
181
+ /**
182
+ * Base address of a three.js 0.182 copy — the folder holding `build/three.module.js` and
183
+ * `examples/jsm/` — so the scene works offline and without a third-party CDN. A relative address
184
+ * resolves against the page and keeps the scene's import map (and its CSP hash) the same wherever
185
+ * the app is installed. Default: `quantumBackgroundThreeUrl` (unpkg).
186
+ */
187
+ threeUrl?: string;
165
188
  /**
166
189
  * Sound the scene cannot hear itself — a native audio engine, a WebRTC call, a server stream: band
167
190
  * levels 0–1 from low to high frequencies, log-spaced across `spectrumRange`. Any band count works;
package/dist/index.d.ts CHANGED
@@ -122,7 +122,7 @@ export { NeonWaves } from "./components/artwork/NeonWaves/NeonWaves";
122
122
  export type { NeonWavesProps } from "./components/artwork/NeonWaves/NeonWaves";
123
123
  export { QuantumField, quantumFieldModes, quantumFieldResolution, quantumFieldAudio } from "./components/artwork/QuantumField/QuantumField";
124
124
  export type { QuantumFieldProps, QuantumFieldMode, QuantumFieldQuality, QuantumAudioFrame } from "./components/artwork/QuantumField/QuantumField";
125
- export { QuantumBackground, QuantumFieldExperience, quantumBackgroundControls, quantumBackgroundDefaults, quantumBackgroundFields, quantumBackgroundFullscreenFieldSize, quantumBackgroundMaxParticles, quantumFieldExperienceControls } from "./components/artwork/QuantumField/QuantumFieldExperience";
125
+ export { QuantumBackground, QuantumFieldExperience, quantumBackgroundControls, quantumBackgroundDefaults, quantumBackgroundFields, quantumBackgroundFullscreenFieldSize, quantumBackgroundMaxParticles, quantumBackgroundThreeUrl, quantumFieldExperienceControls } from "./components/artwork/QuantumField/QuantumFieldExperience";
126
126
  export type { QuantumBackgroundProps, QuantumBackgroundSettings, QuantumBackgroundSettingKey, QuantumBackgroundField, QuantumBackgroundControl, QuantumBackgroundHandle, QuantumBackgroundTransport, QuantumFieldExperienceProps } from "./components/artwork/QuantumField/QuantumFieldExperience";
127
127
  export { createQuantumFieldAudio } from "./components/artwork/QuantumField/audio";
128
128
  export { Spectrum } from "./components/artwork/Spectrum/Spectrum";
package/dist/index.js CHANGED
@@ -5076,10 +5076,15 @@ function QuantumField({
5076
5076
  import { forwardRef as forwardRef3, useCallback, useEffect as useEffect9, useImperativeHandle, useLayoutEffect as useLayoutEffect5, useRef as useRef26, useState as useState10 } from "react";
5077
5077
 
5078
5078
  // src/components/artwork/QuantumField/reference.html?raw
5079
- var reference_default = "<!DOCTYPE html>\n<!-- Quantum Fields Contributors, MIT License. Full notice: UPSTREAM-LICENSE.txt in this package. -->\n<html lang=\"en\">\n\n<head>\n <meta charset=\"UTF-8\">\n <title>Quantum Field: Interactive</title>\n <style>\n @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=Space+Grotesk:wght@400;500;600&display=swap');\n\n * {\n box-sizing: border-box;\n }\n\n :root {\n --ar: 167; --ag: 139; --ab: 250;\n }\n\n html, body {\n margin: 0;\n overflow: hidden;\n background: transparent !important;\n color-scheme: dark;\n font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;\n }\n\n canvas {\n display: block;\n }\n\n #scanlines {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n background: radial-gradient(ellipse at center, transparent 0%, transparent 50%, rgba(0, 0, 0, 0.5) 100%);\n pointer-events: none;\n z-index: 10;\n }\n\n #overlay {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n justify-content: center;\n align-items: center;\n background: linear-gradient(180deg, rgba(15, 10, 25, 0.97) 0%, rgba(10, 10, 20, 0.98) 100%);\n z-index: 100;\n backdrop-filter: blur(20px);\n transition: opacity 1s ease;\n pointer-events: none;\n /* Allow clicks to pass through to canvas */\n }\n\n .hud-panel {\n background: linear-gradient(165deg, rgba(255, 255, 255, 0.04) 0%, rgba(255, 255, 255, 0.01) 100%);\n border: 1px solid rgba(255, 255, 255, 0.07);\n padding: 55px 75px;\n text-align: center;\n border-radius: 28px;\n box-shadow: 0 30px 80px rgba(0, 0, 0, 0.5);\n pointer-events: auto;\n /* Re-enable clicks for buttons */\n }\n\n h1 {\n font-family: 'Space Grotesk', sans-serif;\n font-weight: 500;\n letter-spacing: 1px;\n font-size: 34px;\n margin: 0 0 8px 0;\n background: linear-gradient(135deg, #ffffff 0%, rgba(var(--ar),var(--ag),var(--ab),0.75) 40%, rgb(var(--ar),var(--ag),var(--ab)) 100%);\n -webkit-background-clip: text;\n -webkit-text-fill-color: transparent;\n background-clip: text;\n }\n\n .hud-panel p {\n color: rgba(255, 255, 255, 0.35);\n font-size: 13px;\n margin-bottom: 40px;\n letter-spacing: 0.5px;\n font-weight: 300;\n }\n\n .btn-group {\n display: flex;\n gap: 14px;\n justify-content: center;\n }\n\n button,\n .file-upload {\n background: linear-gradient(135deg, rgba(var(--ar),var(--ag),var(--ab),0.12) 0%, rgba(var(--ar),var(--ag),var(--ab),0.06) 100%);\n border: 1px solid rgba(var(--ar),var(--ag),var(--ab),0.25);\n color: rgba(255, 255, 255, 0.85);\n padding: 15px 30px;\n font-size: 13px;\n font-weight: 500;\n cursor: pointer;\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\n letter-spacing: 0.3px;\n border-radius: 14px;\n font-family: 'Inter', sans-serif;\n }\n\n button:hover,\n .file-upload:hover {\n background: linear-gradient(135deg, rgba(var(--ar),var(--ag),var(--ab),0.25) 0%, rgba(var(--ar),var(--ag),var(--ab),0.18) 100%);\n border-color: rgba(var(--ar),var(--ag),var(--ab),0.45);\n transform: translateY(-2px);\n box-shadow: 0 12px 35px rgba(139, 92, 246, 0.2);\n }\n\n input[type=\"file\"] {\n display: none;\n }\n\n #status-bar {\n position: absolute;\n bottom: 80px;\n left: 28px;\n color: rgba(255, 255, 255, 0.3);\n font-size: 10px;\n letter-spacing: 0.5px;\n z-index: 200;\n pointer-events: none;\n }\n\n #status-bar span {\n color: rgba(196, 181, 253, 0.7);\n font-weight: 500;\n }\n\n #interaction-hint {\n position: absolute;\n top: 28px;\n width: 100%;\n text-align: center;\n color: rgba(255, 255, 255, 0.5);\n font-size: 11px;\n letter-spacing: 2px;\n pointer-events: none;\n z-index: 9;\n text-transform: lowercase;\n }\n\n #zoom-indicator {\n position: absolute;\n bottom: 80px;\n right: 28px;\n color: rgba(255, 255, 255, 0.3);\n font-size: 10px;\n z-index: 20;\n pointer-events: none;\n }\n\n #zoom-indicator span {\n color: rgba(196, 181, 253, 0.7);\n }\n\n #audio-controls {\n position: fixed;\n bottom: 24px;\n left: 50%;\n transform: translateX(-50%);\n display: flex;\n gap: 10px;\n align-items: center;\n padding: 12px 20px;\n background: linear-gradient(180deg, rgba(35, 28, 50, 0.88) 0%, rgba(22, 20, 38, 0.92) 100%);\n border: 1px solid rgba(255, 255, 255, 0.05);\n border-radius: 18px;\n z-index: 200;\n opacity: 0;\n pointer-events: none;\n transition: all 0.4s ease;\n backdrop-filter: blur(25px);\n }\n\n #audio-controls.visible {\n opacity: 1;\n pointer-events: auto;\n }\n\n #audio-controls button,\n #audio-controls .file-upload {\n padding: 9px 14px;\n font-size: 11px;\n background: rgba(255, 255, 255, 0.03);\n border: 1px solid rgba(255, 255, 255, 0.07);\n color: rgba(255, 255, 255, 0.65);\n border-radius: 10px;\n }\n\n #play-pause-btn {\n min-width: 78px;\n 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;\n border-color: rgba(var(--ar),var(--ag),var(--ab),0.28) !important;\n color: #e9d5ff !important;\n }\n\n #play-pause-btn.playing {\n background: linear-gradient(135deg, rgba(251, 113, 133, 0.18) 0%, rgba(244, 114, 182, 0.12) 100%) !important;\n border-color: rgba(251, 113, 133, 0.35) !important;\n }\n\n #progress-container {\n width: 85px;\n height: 4px;\n background: rgba(255, 255, 255, 0.06);\n border-radius: 4px;\n cursor: pointer;\n }\n\n #progress-bar {\n height: 100%;\n 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));\n border-radius: 4px;\n width: 0%;\n }\n\n #time-display {\n color: rgba(255, 255, 255, 0.35);\n font-size: 10px;\n min-width: 62px;\n text-align: center;\n }\n\n #track-name {\n color: rgba(255, 255, 255, 0.3);\n font-size: 10px;\n max-width: 85px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n /* --- Center Panel Refinement --- */\n .hud-panel {\n background: rgba(15, 10, 25, 0.85);\n border: 1px solid rgba(255, 255, 255, 0.08);\n /* Darkened container */\n padding: 24px;\n /* Reduced padding */\n border-radius: 24px;\n box-shadow: 0 20px 50px rgba(0, 0, 0, 0.6);\n backdrop-filter: blur(40px);\n min-width: 420px;\n /* Wider for buttons */\n }\n\n .hud-panel h1,\n .hud-panel p {\n display: none;\n /* Hide title as per screenshot showing only buttons */\n }\n\n .btn-group {\n display: flex;\n gap: 16px;\n justify-content: center;\n width: 100%;\n }\n\n .btn-group button {\n flex: 1;\n background: transparent;\n border: 1px solid rgba(var(--ar),var(--ag),var(--ab),0.3);\n color: rgb(var(--ar),var(--ag),var(--ab));\n padding: 14px 0;\n border-radius: 12px;\n font-size: 12px;\n letter-spacing: 1px;\n text-transform: uppercase;\n font-weight: 600;\n transition: all 0.3s ease;\n }\n\n .btn-group button:hover {\n background: rgba(var(--ar),var(--ag),var(--ab),0.1);\n border-color: rgb(var(--ar),var(--ag),var(--ab));\n box-shadow: 0 0 20px rgba(var(--ar),var(--ag),var(--ab),0.2);\n }\n #interaction-hint, #audio-controls, #status-bar, #zoom-indicator { display: none !important; }\n </style>\n</head>\n\n<body>\n <div id=\"scanlines\" style=\"display:none\"></div>\n <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>\n <div id=\"overlay\" style=\"display:none\">\n <div class=\"hud-panel\">\n <h1>Quantum Fields</h1>\n <p>Audio-reactive particle simulation</p>\n <div class=\"btn-group\">\n <button id=\"enter-btn\">Enter</button>\n </div>\n </div>\n </div>\n <div id=\"audio-controls\">\n <button id=\"play-pause-btn\" class=\"playing\">\u23F8 pause</button>\n <div id=\"progress-container\">\n <div id=\"progress-bar\"></div>\n </div>\n <span id=\"time-display\">0:00 / 0:00</span>\n <span id=\"track-name\">no track</span>\n <label class=\"file-upload\" title=\"Load audio\">\u{1F4C1}<input type=\"file\" id=\"file-input-2\" accept=\"audio/*\"></label>\n <button id=\"mic-btn-2\" title=\"Microphone\">\u{1F399}</button>\n <button id=\"screenshot-btn\" title=\"Screenshot\">\u{1F4F7}</button>\n <button id=\"fullscreen-btn\" title=\"Fullscreen\">\u26F6</button>\n </div>\n <div id=\"status-bar\"><span>ready</span> \xB7 awaiting input</div>\n <div id=\"zoom-indicator\">zoom <span id=\"zoom-level\">100%</span></div>\n\n <!-- Global Error Handler (Must be before module imports) -->\n <script>\n window.onerror = function (msg, url, lineNo, columnNo, error) {\n const status = document.getElementById('status-bar');\n if (status) {\n status.innerHTML = `ERROR: <span style=\"color:#ff6b6b\">${msg}</span>`;\n status.style.opacity = 1;\n }\n console.error('Global Error:', msg, url, lineNo, error);\n return false;\n };\n // Catch unhandled promise rejections\n window.onunhandledrejection = function (event) {\n const status = document.getElementById('status-bar');\n if (status) {\n status.innerHTML = `ERROR: <span style=\"color:#ff6b6b\">${event.reason}</span>`;\n status.style.opacity = 1;\n }\n console.error('Unhandled Rejection:', event.reason);\n };\n <\/script>\n\n <script type=\"importmap\">\n{\n \"imports\": {\n \"three\": \"https://unpkg.com/three@0.182.0/build/three.module.js\",\n \"three/addons/\": \"https://unpkg.com/three@0.182.0/examples/jsm/\"\n }\n}\n<\/script>\n\n <script type=\"module\">\n import * as THREE from 'three';\n import { OrbitControls } from 'three/addons/controls/OrbitControls.js';\n import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';\n import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';\n import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';\n import { AfterimagePass } from 'three/addons/postprocessing/AfterimagePass.js';\n import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';\n // BokehPass removed - using custom DOF shader\n import { OutputPass } from 'three/addons/postprocessing/OutputPass.js';\n\n function fitPixelRatio() {\n return Math.min(window.devicePixelRatio || 1, 1.5,\n Math.sqrt(2_100_000 / Math.max(1, window.innerWidth * window.innerHeight)));\n }\n\n const CONFIG = {\n maxParticles: 250000,\n maxDensity: 0.14,\n secondaryParticles: 50000,\n initialDensity: 0.043,\n fieldRadius: 90,\n minZoom: 1,\n maxZoom: 250,\n defaultZoom: 200\n };\n let particleBudget = Number(window.frameElement?.getAttribute('data-particle-budget') ?? Infinity);\n particleBudget = Number.isFinite(particleBudget) ? Math.max(0, Math.floor(particleBudget)) : Infinity;\n // Requested number of particles across both layers; null follows the density setting.\n let particleCount = window.frameElement?.getAttribute('data-particle-count');\n particleCount = particleCount != null && Number.isFinite(Number(particleCount)) ? Math.max(0, Math.floor(Number(particleCount))) : null;\n\n const STATE = {\n field: 'Gluon (Strong)',\n density: CONFIG.initialDensity,\n fieldSize: 0.78,\n sensitivity: 1.1,\n trails: 0.66144,\n bloom: 0.3,\n timeScale: 1,\n pixelRatio: fitPixelRatio(),\n chromaticAberration: 0.004,\n filmGrain: 0.001,\n cameraShake: 0.65,\n vortexStrength: 0.73,\n pulseIntensity: 0.15,\n anamorphicStretch: 0.1,\n bassGateThreshold: 0.12,\n bassGateAttack: 0.3,\n bassGateRelease: 0.136,\n bassGateRatio: 2.142,\n bassGateEnabled: false,\n // NEW EFFECTS\n godRaysEnabled: false,\n godRaysIntensity: 0.078,\n godRaysDecay: 0.92,\n dofEnabled: false,\n dofFocus: 0.04,\n dofFocalLength: 0.40,\n dofBokehStrength: 0.36,\n lensFlareEnabled: true,\n lensFlareIntensity: 0.1,\n particleTrailsEnabled: true,\n particleTrailOpacity: 1,\n nebulaEnabled: false,\n nebulaIntensity: 0.3,\n connectionsEnabled: true,\n connectionThreshold: 20.0,\n connectionOpacity: 0.75,\n // === ENHANCEMENT PACK v2.0 ===\n // 1. Motion Blur\n motionBlurEnabled: true,\n motionBlurStrength: 0.1,\n // 2. Adaptive Quality\n adaptiveQualityEnabled: true,\n targetFPS: 45,\n qualityLevel: 1,\n // 3. Onset Detection\n onsetSensitivity: 1,\n onsetDecay: 0.92,\n // 4. Audio-Reactive Camera\n audioCameraEnabled: true,\n audioCameraSmoothing: 0.2,\n audioCameraIntensity: 0.3,\n // 5. Waveform Terrain\n waveformTerrainScale: 1.0,\n waveformTerrainHeight: 20.0\n };\n\n // --- AUDIO SYSTEM ---\n // Spectrum supplied by the host app (e.g. a native audio engine the iframe cannot hear):\n // band levels 0..1, log-spaced from `low` to `high` Hz. It is turned into the same frequency\n // array an analyser would give, so bands, beats, flux, onsets and the terrain all follow it.\n const EXTERNAL = {\n target: null, low: 40, high: 12000,\n sampleRate: 48000, data: new Uint8Array(2048), smoothed: new Float32Array(2048), prev: new Float32Array(2048),\n fresh() { return this.target !== null; },\n set(bands, range) {\n this.target = Array.isArray(bands) && bands.length ? bands.map((v) => Math.max(0, Math.min(1, Number(v) || 0))) : null;\n if (Array.isArray(range) && range[0] > 0 && range[1] > range[0]) [this.low, this.high] = range;\n },\n // Host frames arrive at their own pace (often 20 Hz); easing toward them every rendered\n // frame keeps the flux continuous, like an analyser's smoothing does.\n fill() {\n const bands = this.target, n = bands.length, len = this.data.length, nyquist = this.sampleRate / 2;\n const span = Math.log(this.high / this.low);\n for (let i = 0; i < len; i++) {\n const freq = Math.max(1, (i + 0.5) / len * nyquist);\n const pos = Math.log(freq / this.low) / span * (n - 1);\n let level;\n if (pos <= 0) level = bands[0];\n else if (pos >= n - 1) level = bands[n - 1] * Math.max(0, 1 - Math.log(freq / this.high) / Math.log(nyquist / this.high));\n else { const k = Math.floor(pos), t = pos - k; level = bands[k] * (1 - t) + bands[k + 1] * t; }\n this.smoothed[i] += (level - this.smoothed[i]) * 0.35;\n this.data[i] = Math.min(255, Math.round(this.smoothed[i] * 255));\n }\n }\n };\n\n const AUDIO = {\n ctx: null, analyser: null, source: null, gainNode: null, data: null,\n audioBuffer: null, isPlaying: false, isPaused: false,\n startTime: 0, pauseTime: 0, currentTrackName: 'No Track',\n mode: null, micStream: null,\n bands: { subBass: 0, bass: 0, lowMid: 0, mid: 0, highMid: 0, high: 0, ultraHigh: 0 },\n gatedBands: { subBass: 0, bass: 0, lowMid: 0, mid: 0, highMid: 0, high: 0, ultraHigh: 0 },\n beatThreshold: 0.15, beatDecay: 0.93, beatEnergy: 0,\n lastBeatTime: 0, beatDetected: false, peakHistory: [],\n spectralCentroid: 0, spectralFlux: 0, prevSpectrum: null, active: false, idleCleared: false,\n // === ENHANCEMENT: Onset Detection ===\n onsetEnergy: 0, onsetDetected: false, onsetHistory: [], onsetThreshold: 0,\n // === ENHANCEMENT: Waveform Data ===\n waveformData: new Float32Array(128), waveformSmoothed: new Float32Array(128),\n gateState: {\n isOpen: false, envelope: 0, holdTime: 0, noiseFloor: 0.05,\n calibrationSamples: [], isCalibrating: true, calibrationFrames: 30\n },\n\n init() {\n if (this.ctx) return;\n this.ctx = new (window.AudioContext || window.webkitAudioContext)();\n this.analyser = this.ctx.createAnalyser();\n this.analyser.fftSize = 4096;\n this.analyser.smoothingTimeConstant = 0.65;\n this.data = new Uint8Array(this.analyser.frequencyBinCount);\n this.prevSpectrum = new Float32Array(this.analyser.frequencyBinCount);\n this.active = true;\n },\n\n loadTrack(arrayBuffer, fileName) {\n return new Promise((resolve, reject) => {\n this.ctx.decodeAudioData(arrayBuffer, (buffer) => {\n this.stopCurrentSource();\n this.audioBuffer = buffer;\n this.currentTrackName = fileName || 'Unknown';\n this.mode = 'file';\n this.pauseTime = 0;\n this.resetCalibration();\n resolve(buffer);\n }, reject);\n });\n },\n\n play() {\n if (!this.audioBuffer || this.mode !== 'file' || this.isPlaying) return;\n this.source = this.ctx.createBufferSource();\n this.source.buffer = this.audioBuffer;\n this.source.loop = true;\n this.source.connect(this.analyser);\n this.analyser.connect(this.ctx.destination);\n this.source.start(0, this.pauseTime);\n this.startTime = this.ctx.currentTime - this.pauseTime;\n this.isPlaying = true;\n this.isPaused = false;\n },\n\n pause() {\n if (!this.isPlaying || this.mode !== 'file') return;\n this.pauseTime = this.getCurrentTime();\n this.isPaused = true;\n this.isPlaying = false;\n if (this.source) { try { this.source.stop(); } catch (e) { } this.source.disconnect(); this.source = null; }\n },\n\n togglePlayPause() {\n if (this.mode === 'mic') return false;\n if (this.isPlaying) { this.pause(); return false; }\n else { this.play(); return true; }\n },\n\n getCurrentTime() {\n if (!this.isPlaying) return this.pauseTime;\n if (!this.audioBuffer) return 0;\n return (this.ctx.currentTime - this.startTime) % this.audioBuffer.duration;\n },\n\n getDuration() { return this.audioBuffer ? this.audioBuffer.duration : 0; },\n\n seek(time) {\n if (this.mode !== 'file' || !this.audioBuffer) return;\n const wasPlaying = this.isPlaying;\n if (wasPlaying) this.pause();\n this.pauseTime = Math.max(0, Math.min(time, this.audioBuffer.duration));\n if (wasPlaying) this.play();\n },\n\n stopCurrentSource() {\n if (this.source) { try { this.source.stop(); this.source.disconnect(); } catch (e) { } this.source = null; }\n if (this.gainNode) { try { this.gainNode.disconnect(); } catch (e) { } this.gainNode = null; }\n if (this.micStream) { this.micStream.getTracks().forEach(t => t.stop()); this.micStream = null; }\n this.isPlaying = false; this.isPaused = false; this.pauseTime = 0;\n },\n\n async startMic(preferredLabel = null) {\n console.log('startMic called with preferredLabel:', preferredLabel);\n const status = document.getElementById('status-bar');\n if (status) status.innerHTML = 'SYSTEM: <span>REQUESTING ACCESS...</span>';\n\n try {\n // Ensure context is running\n if (this.ctx.state === 'suspended') {\n console.log('Resuming AudioContext...');\n await this.ctx.resume();\n }\n\n // 1. Get initial stream (activates permissions so labels are visible)\n console.log('Requesting initial getUserMedia...');\n let stream = await navigator.mediaDevices.getUserMedia({ audio: true });\n console.log('Initial stream obtained:', stream.id);\n\n // 2. Search for preferred device (loopback)\n let target = null;\n if (preferredLabel) {\n const devices = await navigator.mediaDevices.enumerateDevices();\n console.log(\"Audio Devices Found:\", devices.length, devices.map(d => `${d.label} (${d.kind})`));\n\n target = devices.find(d => d.kind === 'audioinput' &&\n d.label.toLowerCase().includes(preferredLabel.toLowerCase()));\n }\n\n if (target) {\n console.log(`Switching audio input to: ${target.label} [${target.deviceId}]`);\n stream.getTracks().forEach(t => t.stop()); // Stop generic stream\n\n // Request specific device - being explicit with constraints\n stream = await navigator.mediaDevices.getUserMedia({\n audio: {\n deviceId: { exact: target.deviceId },\n echoCancellation: false,\n noiseSuppression: false,\n autoGainControl: false,\n channelCount: 2\n }\n });\n this.currentTrackName = target.label;\n } else {\n if (preferredLabel) console.warn(`Preferred device '${preferredLabel}' not found. Using default.`);\n // Keep the initial stream\n this.currentTrackName = stream.getAudioTracks()[0]?.label || 'Live Microphone';\n }\n\n this.stopCurrentSource();\n this.micStream = stream;\n this.source = this.ctx.createMediaStreamSource(stream);\n this.gainNode = this.ctx.createGain();\n this.gainNode.gain.value = 3.0; // Boost loopback/mic signal before FFT \u2014 tune via sensitivity slider\n this.source.connect(this.gainNode);\n this.gainNode.connect(this.analyser);\n this.mode = 'mic';\n this.isPlaying = true;\n this.resetCalibration();\n\n if (status) status.innerHTML = 'SYSTEM: <span>LISTENING (' + (preferredLabel ? 'Loopback' : 'Mic') + ')</span>';\n updateUI();\n\n return stream;\n } catch (e) {\n console.error(\"AUDIO.startMic failed:\", e);\n throw e;\n }\n },\n\n resetCalibration() { this.gateState.isCalibrating = true; this.gateState.calibrationSamples = []; },\n\n applyBassGate(rawValue) {\n if (!STATE.bassGateEnabled) return rawValue;\n const { bassGateThreshold: threshold, bassGateAttack: attack, bassGateRelease: release, bassGateRatio: ratio } = STATE;\n if (this.gateState.isCalibrating) {\n this.gateState.calibrationSamples.push(rawValue);\n if (this.gateState.calibrationSamples.length >= this.gateState.calibrationFrames) {\n const sorted = [...this.gateState.calibrationSamples].sort((a, b) => a - b);\n this.gateState.noiseFloor = sorted.slice(0, Math.floor(sorted.length * 0.2)).reduce((a, b) => a + b, 0) / Math.floor(sorted.length * 0.2);\n this.gateState.isCalibrating = false;\n }\n }\n const effectiveThreshold = Math.max(threshold, this.gateState.noiseFloor * 1.5);\n if (rawValue > effectiveThreshold) { this.gateState.isOpen = true; this.gateState.holdTime = 10; }\n else if (rawValue < effectiveThreshold * 0.7 && this.gateState.holdTime <= 0) { this.gateState.isOpen = false; }\n if (this.gateState.holdTime > 0) this.gateState.holdTime--;\n this.gateState.envelope += ((this.gateState.isOpen ? 1.0 : 0.0) - this.gateState.envelope) * (this.gateState.isOpen ? attack : release);\n let output = rawValue;\n if (this.gateState.isOpen && rawValue > effectiveThreshold) {\n output = effectiveThreshold + (rawValue - effectiveThreshold) / ratio;\n }\n return output * this.gateState.envelope;\n },\n\n update() {\n const own = this.active && this.isPlaying && this.source;\n this.externalActive = !own && EXTERNAL.fresh();\n if (!own && !this.externalActive) {\n if (!this.idleCleared) {\n for (const band in this.bands) this.bands[band] = this.gatedBands[band] = 0;\n this.spectralCentroid = this.spectralFlux = 0;\n this.beatDetected = this.onsetDetected = false;\n this.idleCleared = true;\n }\n this.beatEnergy *= this.beatDecay;\n this.onsetEnergy *= STATE.onsetDecay;\n return;\n }\n this.idleCleared = false;\n let data = this.data, prevSpectrum = this.prevSpectrum, nyquist;\n if (this.externalActive) {\n EXTERNAL.fill();\n data = EXTERNAL.data; prevSpectrum = EXTERNAL.prev; nyquist = EXTERNAL.sampleRate / 2;\n } else {\n this.analyser.getByteFrequencyData(this.data);\n nyquist = this.ctx.sampleRate / 2;\n }\n const len = data.length;\n const getFreqIndex = (freq) => Math.round(freq / nyquist * len);\n const ranges = { subBass: [50, 120], bass: [120, 200], lowMid: [200, 450], mid: [450, 2000], highMid: [2000, 3500], high: [3500, 12000], ultraHigh: [12000, 20000] };\n let totalEnergy = 0, weightedFreqSum = 0;\n for (const [band, [low, high]] of Object.entries(ranges)) {\n const lowIdx = getFreqIndex(low), highIdx = getFreqIndex(high);\n let sum = 0;\n for (let i = lowIdx; i < highIdx && i < len; i++) {\n const v = data[i] / 255.0;\n sum += v; totalEnergy += v; weightedFreqSum += v * i;\n }\n const rawBand = sum / (highIdx - lowIdx);\n this.bands[band] = rawBand;\n this.gatedBands[band] = (band === 'subBass' || band === 'bass' || band === 'lowMid') ? this.applyBassGate(rawBand) : rawBand;\n }\n this.spectralCentroid = totalEnergy > 0 ? weightedFreqSum / totalEnergy / len : 0;\n let flux = 0;\n for (let i = 0; i < len; i++) {\n const current = data[i] / 255.0, diff = current - prevSpectrum[i];\n flux += diff > 0 ? diff * diff : 0;\n prevSpectrum[i] = current;\n }\n this.spectralFlux = Math.sqrt(flux / len);\n const currentEnergy = this.gatedBands.bass * 0.6 + this.gatedBands.subBass * 0.4;\n this.peakHistory.push(currentEnergy);\n if (this.peakHistory.length > 50) this.peakHistory.shift();\n const avgEnergy = this.peakHistory.reduce((a, b) => a + b, 0) / this.peakHistory.length;\n const variance = this.peakHistory.reduce((a, b) => a + Math.pow(b - avgEnergy, 2), 0) / this.peakHistory.length;\n const dynamicThreshold = avgEnergy + Math.sqrt(variance) * 1.8;\n const now = performance.now();\n if (currentEnergy > dynamicThreshold && currentEnergy > this.beatThreshold && now - this.lastBeatTime > 120) {\n this.beatDetected = true; this.beatEnergy = 1.0; this.lastBeatTime = now;\n } else { this.beatDetected = false; }\n this.beatEnergy *= this.beatDecay;\n\n // === ENHANCEMENT: Improved Onset Detection ===\n // Uses spectral flux derivative for sharper transient response\n const onsetSens = STATE.onsetSensitivity;\n this.onsetHistory.push(this.spectralFlux);\n if (this.onsetHistory.length > 8) this.onsetHistory.shift();\n const recentAvg = this.onsetHistory.slice(-4).reduce((a, b) => a + b, 0) / 4;\n const olderAvg = this.onsetHistory.slice(0, 4).reduce((a, b) => a + b, 0) / Math.max(1, this.onsetHistory.slice(0, 4).length);\n const onsetDelta = (recentAvg - olderAvg) * onsetSens;\n this.onsetDetected = onsetDelta > 0.015 && now - this.lastBeatTime > 50;\n this.onsetEnergy = this.onsetDetected ? 1.0 : this.onsetEnergy * STATE.onsetDecay;\n\n // === ENHANCEMENT: Waveform Data for Terrain Field ===\n const waveformBins = 128;\n const binSize = Math.floor(len / waveformBins);\n for (let i = 0; i < waveformBins; i++) {\n let sum = 0;\n for (let j = 0; j < binSize; j++) {\n sum += data[i * binSize + j] / 255.0;\n }\n const target = sum / binSize;\n this.waveformSmoothed[i] += (target - this.waveformSmoothed[i]) * 0.3;\n this.waveformData[i] = this.waveformSmoothed[i];\n }\n }\n };\n\n // --- SCENE SETUP ---\n const scene = new THREE.Scene();\n scene.fog = new THREE.FogExp2(0x000508, 0.005);\n const camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 0.1, 800);\n camera.position.set(0, 12, CONFIG.defaultZoom);\n const cameraShakeOffset = new THREE.Vector3();\n const baseCameraPos = camera.position.clone();\n const zoomIndicator = document.getElementById('zoom-level');\n\n const renderer = new THREE.WebGLRenderer({ powerPreference: \"high-performance\", antialias: true, alpha: true });\n renderer.setClearColor(0x000000, 0);\n renderer.setSize(window.innerWidth, window.innerHeight);\n renderer.setPixelRatio(STATE.pixelRatio);\n renderer.toneMapping = THREE.ACESFilmicToneMapping;\n renderer.toneMappingExposure = 2.1;\n document.body.appendChild(renderer.domElement);\n\n const controls = new OrbitControls(camera, renderer.domElement);\n controls.enableDamping = true;\n controls.dampingFactor = 0.06;\n controls.autoRotate = false;\n controls.autoRotateSpeed = 0.3;\n controls.minDistance = CONFIG.minZoom;\n controls.maxDistance = CONFIG.maxZoom;\n controls.maxPolarAngle = Math.PI * 0.85;\n controls.minPolarAngle = Math.PI * 0.15;\n\n controls.addEventListener('change', () => {\n const dist = camera.position.length();\n const zoomPercent = Math.round((1 - (dist - CONFIG.minZoom) / (CONFIG.maxZoom - CONFIG.minZoom)) * 100);\n zoomIndicator.textContent = Math.max(0, Math.min(100, zoomPercent)) + '%';\n baseCameraPos.copy(camera.position);\n });\n\n const raycaster = new THREE.Raycaster();\n const mouse = new THREE.Vector2(9999, 9999);\n const plane = new THREE.Plane(new THREE.Vector3(0, 0, 1), 0);\n const mouseWorldPos = new THREE.Vector3();\n let mouseVelocity = new THREE.Vector2();\n let prevMouse = new THREE.Vector2();\n\n window.addEventListener('mousemove', (e) => {\n const newMouse = new THREE.Vector2((e.clientX / window.innerWidth) * 2 - 1, -(e.clientY / window.innerHeight) * 2 + 1);\n mouseVelocity.subVectors(newMouse, prevMouse);\n prevMouse.copy(newMouse);\n mouse.copy(newMouse);\n raycaster.setFromCamera(mouse, camera);\n const target = new THREE.Vector3();\n raycaster.ray.intersectPlane(plane, target);\n if (target) mouseWorldPos.copy(target);\n });\n\n window.addEventListener('keydown', (e) => {\n if (e.key === '+' || e.key === '=') { camera.position.multiplyScalar(0.9); controls.update(); }\n else if (e.key === '-' || e.key === '_') { camera.position.multiplyScalar(1.1); controls.update(); }\n else if (e.key === '0') { camera.position.normalize().multiplyScalar(CONFIG.defaultZoom); controls.update(); }\n else if (e.key === ' ') { e.preventDefault(); togglePlayPause(); }\n else if (e.key === 'f' || e.key === 'F') { STATE.dofEnabled = !STATE.dofEnabled; customDOFPass.enabled = STATE.dofEnabled; }\n });\n\n // Every field type shares one screen-fitted footprint; switching presets never reallocates particles.\n // uFieldFill: share of the viewport the field spans; above 1.0 it runs past the edges.\n const shapeFunction = `\nuniform float uFieldFill;\nvec3 shapedPosition(vec3 seed) {\n vec2 plane = seed.xy * 2.0 - 1.0;\n return vec3(plane.x * uViewportAspect * uViewportHalfHeight * uFieldFill,\n plane.y * uViewportHalfHeight * uFieldFill,\n (seed.z - 0.5) * 12.0);\n}`;\n\n // --- PARTICLE VERTEX SHADER ---\n const vertexShader = `\nuniform float uTime, uPixelRatio, uSizeBase, uNoiseScale, uCurlStrength;\nuniform float uViewportAspect, uViewportHalfHeight;\nuniform vec3 uColor1, uColor2, uColor3;\nuniform float uSubBass, uBass, uLowMid, uMid, uHighMid, uHigh, uUltraHigh;\nuniform float uBeatEnergy, uSpectralCentroid, uSpectralFlux;\nuniform float uOnsetEnergy, uTerrainMode, uTerrainHeight;\nuniform float uVortexStrength, uPulseIntensity, uZoomFactor;\nuniform vec3 uMousePos;\nuniform vec2 uMouseVelocity;\nattribute vec3 aRandom;\nattribute float aPhase, aLayer;\nvarying vec3 vColor;\nvarying float vAlpha, vEnergy, vDepth, vRimLight, vGlint;\n\nvec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }\nvec4 mod289(vec4 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }\nvec4 permute(vec4 x) { return mod289(((x*34.0)+1.0)*x); }\nvec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; }\n\nfloat snoise(vec3 v) {\n const vec2 C = vec2(1.0/6.0, 1.0/3.0);\n const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);\n vec3 i = floor(v + dot(v, C.yyy));\n vec3 x0 = v - i + dot(i, C.xxx);\n vec3 g = step(x0.yzx, x0.xyz);\n vec3 l = 1.0 - g;\n vec3 i1 = min(g.xyz, l.zxy);\n vec3 i2 = max(g.xyz, l.zxy);\n vec3 x1 = x0 - i1 + C.xxx;\n vec3 x2 = x0 - i2 + C.yyy;\n vec3 x3 = x0 - D.yyy;\n i = mod289(i);\n 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));\n float n_ = 0.142857142857;\n vec3 ns = n_ * D.wyz - D.xzx;\n vec4 j = p - 49.0 * floor(p * ns.z * ns.z);\n vec4 x_ = floor(j * ns.z);\n vec4 y_ = floor(j - 7.0 * x_);\n vec4 x = x_ * ns.x + ns.yyyy;\n vec4 y = y_ * ns.x + ns.yyyy;\n vec4 h = 1.0 - abs(x) - abs(y);\n vec4 b0 = vec4(x.xy, y.xy);\n vec4 b1 = vec4(x.zw, y.zw);\n vec4 s0 = floor(b0) * 2.0 + 1.0;\n vec4 s1 = floor(b1) * 2.0 + 1.0;\n vec4 sh = -step(h, vec4(0.0));\n vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy;\n vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww;\n vec3 p0 = vec3(a0.xy, h.x);\n vec3 p1 = vec3(a0.zw, h.y);\n vec3 p2 = vec3(a1.xy, h.z);\n vec3 p3 = vec3(a1.zw, h.w);\n vec4 norm = taylorInvSqrt(vec4(dot(p0, p0), dot(p1, p1), dot(p2, p2), dot(p3, p3)));\n p0 *= norm.x; p1 *= norm.y; p2 *= norm.z; p3 *= norm.w;\n vec4 m = max(0.6 - vec4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), 0.0);\n m = m * m;\n return 42.0 * dot(m * m, vec4(dot(p0, x0), dot(p1, x1), dot(p2, x2), dot(p3, x3)));\n}\n\nvec3 curl(float x, float y, float z) {\n float eps = 0.08;\n return vec3(\n snoise(vec3(x, y+eps, z)) - snoise(vec3(x, y-eps, z)),\n snoise(vec3(x, y, z+eps)) - snoise(vec3(x, y, z-eps)),\n snoise(vec3(x+eps, y, z)) - snoise(vec3(x-eps, y, z))\n );\n}\n\n${shapeFunction}\n\nvoid main() {\n float t = uTime * (1.0 + uMid * 0.5);\n vec3 noisePos = position * 0.035 + aRandom;\n float layerMod = 1.0 + aLayer * 0.5;\n\n vec3 flow1 = curl(noisePos.x * uNoiseScale + t*0.1, noisePos.y * uNoiseScale, noisePos.z * uNoiseScale);\n 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;\n vec3 flow3 = curl(noisePos.x * uNoiseScale * 0.5 - t*0.05, noisePos.y * uNoiseScale * 0.5, noisePos.z * uNoiseScale * 0.5) * 0.6 * uLowMid;\n vec3 flow = flow1 + flow2 * uHigh + flow3;\n\n vec3 newPos = shapedPosition(aRandom) + (flow * uCurlStrength * layerMod * (1.0 + uSpectralFlux * 2.5));\n\n float vortexAngle = uVortexStrength * uBass * 0.45 * (0.75 + aRandom.z * 0.5);\n float cosA = cos(vortexAngle), sinA = sin(vortexAngle);\n vec2 rotated = vec2(newPos.x * cosA - newPos.z * sinA, newPos.x * sinA + newPos.z * cosA);\n newPos.xz = mix(newPos.xz, rotated, uBass * uVortexStrength);\n\n float yVortex = sin(newPos.y * 0.1 + uTime) * uMid * uVortexStrength * 2.0;\n newPos.x += cos(newPos.y * 0.15) * yVortex;\n newPos.z += sin(newPos.y * 0.15) * yVortex;\n\n float distToMouse = distance(newPos, uMousePos);\n float repulsion = smoothstep(25.0, 0.0, distToMouse);\n vec3 repelDir = normalize(newPos - uMousePos + vec3(0.001));\n newPos += repelDir * repulsion * (12.0 + length(uMouseVelocity) * 360.0);\n\n // Audio changes depth throughout the plane, without evacuating its centre.\n float beatWave = sin(aRandom.x * 12.0 + aRandom.y * 7.0 - uTime * 6.0);\n float bassDepth = (beatWave * uBeatEnergy * uPulseIntensity * 3.5\n + sin(uTime * 1.8 + aPhase) * uSubBass * 4.0\n + smoothstep(0.3, 0.8, uBass) * 6.0\n + uOnsetEnergy * 8.0) * (0.6 + aRandom.z * 0.4);\n newPos.z += bassDepth;\n float fieldWave = sin(aRandom.x * 11.0 - uTime * 0.8 + sin(aRandom.y * 7.0 + uTime * 0.35));\n newPos.z += fieldWave * (1.8 + uBass * 2.4 + uBeatEnergy * 1.6);\n\n // Waveform relief moves toward the camera while retaining the shared rectangular footprint.\n if (uTerrainMode > 0.5) {\n float freqBin = aRandom.x;\n float waveHeight = uBass * sin(freqBin * 6.28 * 8.0 + uTime * 3.0) * 0.5;\n waveHeight += uMid * sin(freqBin * 6.28 * 16.0 + uTime * 5.0) * 0.3;\n waveHeight += uHigh * sin(freqBin * 6.28 * 32.0 + uTime * 8.0) * 0.2;\n waveHeight *= (1.0 + uSpectralFlux * 3.0);\n newPos.z += waveHeight * uTerrainHeight;\n newPos.z += sin(aRandom.y * 6.28 * 4.0 + uTime) * uSubBass * 5.0;\n }\n\n vec4 mvPosition = modelViewMatrix * vec4(newPos, 1.0);\n gl_Position = projectionMatrix * mvPosition;\n\n float size = uSizeBase * (0.85 + uBass * 0.5 + uBeatEnergy * 0.5) * (1.0 + (uZoomFactor - 1.0) * 0.3);\n size *= 1.0 + sin(uTime * 10.0 + aPhase * 6.28) * uHigh * 0.35;\n 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);\n size *= 0.85 + vGlint * 2.1;\n gl_PointSize = clamp(max(size * uPixelRatio * (90.0 / -mvPosition.z) * layerMod, vGlint * 4.5 * uPixelRatio), 0.5, 120.0);\n\n float colorMix = smoothstep(0.0, 2.8, length(flow) + uHighMid * 3.0);\n vec3 baseColor = mix(uColor1, uColor2, colorMix);\n baseColor = mix(baseColor, uColor3, uSpectralCentroid * uHigh * 2.5);\n\n float energyLevel = uBeatEnergy * 0.35 + repulsion * uMid * 2.0 + uSpectralFlux * 0.8;\n vColor = mix(baseColor, vec3(1.0), energyLevel * 0.4);\n vColor += vec3(0.08, 0.04, 0.12) * uUltraHigh * 2.5;\n\n vRimLight = pow(1.0 - max(0.0, dot(normalize(-mvPosition.xyz), normalize(newPos))), 3.0);\n vEnergy = energyLevel;\n vDepth = clamp(-mvPosition.z / 120.0, 0.0, 1.0);\n\n float alpha = 1.0 - smoothstep(0.92, 1.05, max(abs(newPos.x) / (uViewportAspect * uViewportHalfHeight * uFieldFill * 1.05), abs(newPos.y) / (uViewportHalfHeight * uFieldFill * 1.05)));\n float sparkle = 1.0 + (aRandom.x > 0.8 ? sin(uTime * 20.0 + aRandom.y * 150.0) * uHigh * 2.5 : 0.0);\n vAlpha = alpha * sparkle * (0.7 + uMid * 0.35) * layerMod;\n}`;\n\n const fragmentShader = `\nvarying vec3 vColor;\nvarying float vAlpha, vEnergy, vDepth, vRimLight, vGlint;\n\nvoid main() {\n vec2 xy = gl_PointCoord.xy - vec2(0.5);\n float r = length(xy);\n if(r > 0.5) discard;\n\n float core = smoothstep(0.16 - vEnergy * 0.04, 0.12 - vEnergy * 0.04, r);\n float glow = exp(-r * r * (20.0 - vEnergy * 10.0));\n float halo = exp(-r * r * 5.0) * 0.35 + exp(-r * r * 2.5) * 0.15;\n 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));\n\n vec3 finalColor = mix(vColor, vec3(1.0), core * 0.58 + glint * 0.38);\n finalColor += vColor * vRimLight * 0.1;\n finalColor = mix(finalColor, vec3(0.02, 0.03, 0.05), vDepth * 0.4);\n\n 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);\n gl_FragColor = vec4(finalColor, finalAlpha);\n}`;\n\n // \u2550\u2550\u2550\u2550\u2550\u2550\u2550 POST-PROCESSING SHADERS \u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n // GOD RAYS SHADER (Radial Light Scattering)\n const GodRaysShader = {\n uniforms: {\n tDiffuse: { value: null },\n uCenter: { value: new THREE.Vector2(0.5, 0.5) },\n uIntensity: { value: 0.35 },\n uDecay: { value: 0.94 },\n uBeatEnergy: { value: 0.0 }\n },\n vertexShader: `varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`,\n fragmentShader: `\n uniform sampler2D tDiffuse;\n uniform vec2 uCenter;\n uniform float uIntensity, uDecay, uBeatEnergy;\n varying vec2 vUv;\n\n void main() {\n vec4 color = texture2D(tDiffuse, vUv);\n vec2 dir = vUv - uCenter;\n float dist = length(dir);\n dir = normalize(dir);\n\n vec3 rays = vec3(0.0);\n float decay = 1.0;\n\n for(int i = 0; i < 32; i++) {\n float t = float(i) / 32.0;\n vec2 samplePos = vUv - dir * t * 0.15 * (1.0 + uBeatEnergy);\n vec4 s = texture2D(tDiffuse, samplePos);\n float lum = dot(s.rgb, vec3(0.3, 0.59, 0.11));\n rays += s.rgb * decay * step(0.35, lum);\n decay *= uDecay;\n }\n rays /= 16.0;\n rays *= (1.0 - smoothstep(0.3, 0.9, dist));\n\n gl_FragColor = vec4(color.rgb + rays * uIntensity * (1.0 + uBeatEnergy * 0.8), color.a);\n }`\n };\n\n // LENS FLARE POST-PROCESS SHADER (Cinematic)\n const LensFlareShader = {\n uniforms: {\n tDiffuse: { value: null },\n uIntensity: { value: 1.0 },\n uBeatEnergy: { value: 0.0 },\n uTime: { value: 0.0 },\n uTint: { value: new THREE.Color(0.6, 0.7, 1.0) }\n },\n vertexShader: `varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`,\n fragmentShader: `\n uniform sampler2D tDiffuse;\n uniform float uIntensity, uBeatEnergy, uTime;\n uniform vec3 uTint;\n varying vec2 vUv;\n\n float sdHex(vec2 p) {\n p = abs(p);\n return max(p.x * 0.866 + p.y * 0.5, p.y) - 1.0;\n }\n\n void main() {\n vec4 color = texture2D(tDiffuse, vUv);\n vec2 center = vec2(0.5);\n vec2 toCenter = center - vUv;\n float dist = length(toCenter);\n vec2 dir = normalize(toCenter + 0.0001);\n\n vec3 flare = vec3(0.0);\n\n // Sample bright areas for flare source\n float brightness = dot(color.rgb, vec3(0.299, 0.587, 0.114));\n float threshold = smoothstep(0.6, 0.9, brightness);\n\n // Anamorphic horizontal streak\n float streak = 0.0;\n for(float i = -8.0; i <= 8.0; i += 1.0) {\n vec2 streakUV = vUv + vec2(i * 0.012, 0.0);\n vec4 streakSample = texture2D(tDiffuse, streakUV);\n float streakBright = dot(streakSample.rgb, vec3(0.299, 0.587, 0.114));\n float falloff = 1.0 - abs(i) / 10.0;\n streak += smoothstep(0.7, 1.0, streakBright) * falloff * falloff;\n }\n flare += uTint * streak * 0.08;\n\n // Subtle ghosts (reflections through lens elements)\n for(int i = 1; i <= 3; i++) {\n float scale = 1.0 - float(i) * 0.25;\n vec2 ghostUV = center + (center - vUv) * scale * 0.8;\n if(ghostUV.x > 0.0 && ghostUV.x < 1.0 && ghostUV.y > 0.0 && ghostUV.y < 1.0) {\n vec4 ghostSample = texture2D(tDiffuse, ghostUV);\n float ghostBright = dot(ghostSample.rgb, vec3(0.299, 0.587, 0.114));\n float ghostAlpha = smoothstep(0.5, 0.8, ghostBright) * (0.25 / float(i));\n vec3 ghostTint = mix(vec3(1.0), uTint, 0.6) * (1.0 - float(i) * 0.2);\n flare += ghostSample.rgb * ghostTint * ghostAlpha;\n }\n }\n\n // Soft circular halo around bright center\n float halo = smoothstep(0.25, 0.15, abs(dist - 0.2)) * threshold * 0.25;\n flare += uTint * 0.85 * halo;\n\n // Subtle vignette-edge glow\n float edgeGlow = smoothstep(0.5, 0.7, dist) * (1.0 - smoothstep(0.7, 0.85, dist));\n flare += uTint * 0.55 * edgeGlow * brightness * 0.15;\n\n gl_FragColor = vec4(color.rgb + flare * uIntensity * (1.0 + uBeatEnergy * 0.3), color.a);\n }`\n };\n\n // ANAMORPHIC FLARE SHADER\n const AnamorphicShader = {\n uniforms: {\n tDiffuse: { value: null },\n uStretch: { value: 0.1 },\n uThreshold: { value: 0.7 },\n uIntensity: { value: 0.4 },\n uTint: { value: new THREE.Color(0.6, 0.8, 1.0) }\n },\n vertexShader: `varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`,\n fragmentShader: `\n uniform sampler2D tDiffuse;\n uniform float uStretch, uThreshold, uIntensity;\n uniform vec3 uTint;\n varying vec2 vUv;\n void main() {\n vec4 color = texture2D(tDiffuse, vUv);\n vec3 streak = vec3(0.0);\n for(float i = -15.0; i <= 15.0; i++) {\n vec4 s = texture2D(tDiffuse, vUv + vec2(i * uStretch * 0.01, 0.0));\n float bright = dot(s.rgb, vec3(0.299, 0.587, 0.114));\n if(bright > uThreshold) {\n float w = 1.0 - abs(i) / 15.0;\n streak += s.rgb * w * w;\n }\n }\n gl_FragColor = vec4(color.rgb + streak / 7.5 * uTint * uIntensity, color.a);\n }`\n };\n\n // CHROMATIC ABERRATION SHADER\n const ChromaticShader = {\n uniforms: {\n tDiffuse: { value: null },\n uIntensity: { value: 0.004 },\n uBeatEnergy: { value: 0.0 }\n },\n vertexShader: `varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`,\n fragmentShader: `\n uniform sampler2D tDiffuse;\n uniform float uIntensity, uBeatEnergy;\n varying vec2 vUv;\n void main() {\n vec2 dir = vUv - vec2(0.5);\n float dist = length(dir);\n vec2 offset = dir * dist * uIntensity * (1.0 + uBeatEnergy * 1.5);\n gl_FragColor = vec4(\n texture2D(tDiffuse, vUv + offset * 1.2).r,\n texture2D(tDiffuse, vUv).g,\n texture2D(tDiffuse, vUv - offset * 1.2).b,\n texture2D(tDiffuse, vUv).a\n );\n }`\n };\n\n // FILM GRAIN SHADER\n const GrainShader = {\n uniforms: {\n tDiffuse: { value: null },\n uTime: { value: 0.0 },\n uIntensity: { value: 0.0 }\n },\n vertexShader: `varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`,\n fragmentShader: `\n uniform sampler2D tDiffuse;\n uniform float uTime, uIntensity;\n varying vec2 vUv;\n float rand(vec2 co) { return fract(sin(dot(co, vec2(12.9898,78.233))) * 43758.5453); }\n void main() {\n vec4 color = texture2D(tDiffuse, vUv);\n float grain = (rand(vUv + fract(uTime)) * 2.0 - 1.0) * uIntensity;\n gl_FragColor = vec4(color.rgb + grain * (1.0 - dot(color.rgb, vec3(0.299, 0.587, 0.114)) * 0.5), color.a);\n }`\n };\n\n // CUSTOM DEPTH OF FIELD SHADER (Radial + Edge blur)\n const CustomDOFShader = {\n uniforms: {\n tDiffuse: { value: null },\n uFocus: { value: 0.05 },\n uFocalLength: { value: 0.25 },\n uBokehStrength: { value: 0.5 },\n uResolution: { value: new THREE.Vector2() },\n uBeatEnergy: { value: 0.0 }\n },\n vertexShader: `\n varying vec2 vUv;\n void main() {\n vUv = uv;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n }\n `,\n fragmentShader: `\n uniform sampler2D tDiffuse;\n uniform float uFocus;\n uniform float uFocalLength;\n uniform float uBokehStrength;\n uniform vec2 uResolution;\n uniform float uBeatEnergy;\n varying vec2 vUv;\n\n void main() {\n vec2 center = vec2(0.5);\n float dist = length(vUv - center);\n\n // Focus ring - sharp in a ring around center, blurry inside and outside\n float focusRing = abs(dist - uFocus);\n float blur = smoothstep(0.0, uFocalLength, focusRing) * uBokehStrength;\n\n vec3 color = vec3(0.0);\n float total = 0.0;\n vec2 texel = 1.0 / uResolution;\n\n if(blur < 0.01) {\n color = texture2D(tDiffuse, vUv).rgb;\n total = 1.0;\n } else {\n // Golden angle spiral sampling\n const float GOLDEN_ANGLE = 2.39996323;\n const int SAMPLES = 24;\n float radius = blur * 20.0 * (1.0 + uBeatEnergy * 0.1);\n\n for(int i = 0; i < SAMPLES; i++) {\n float angle = float(i) * GOLDEN_ANGLE;\n float r = sqrt(float(i) / float(SAMPLES)) * radius;\n vec2 offset = vec2(cos(angle), sin(angle)) * r * texel;\n vec3 s = texture2D(tDiffuse, vUv + offset).rgb;\n\n // Subtle highlight boost\n float lum = dot(s, vec3(0.299, 0.587, 0.114));\n s *= 1.0 + smoothstep(0.7, 1.0, lum) * blur * 0.3;\n\n color += s;\n total += 1.0;\n }\n }\n\n gl_FragColor = vec4(color / total, texture2D(tDiffuse, vUv).a);\n }\n `\n };\n\n // VIGNETTE + GLOW SHADER\n const VignetteShader = {\n uniforms: {\n tDiffuse: { value: null },\n uBeatEnergy: { value: 0.0 }\n },\n vertexShader: `varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`,\n fragmentShader: `\n uniform sampler2D tDiffuse;\n uniform float uBeatEnergy;\n varying vec2 vUv;\n void main() {\n vec4 color = texture2D(tDiffuse, vUv);\n vec2 uv = vUv * (1.0 - vUv.yx);\n float vig = pow(uv.x * uv.y * 15.0, 0.38 * (1.0 - uBeatEnergy * 0.15));\n float lum = dot(color.rgb, vec3(0.299, 0.587, 0.114));\n float glow = smoothstep(0.5, 0.9, lum) * 0.1 * (1.0 + uBeatEnergy * 0.5);\n gl_FragColor = vec4(color.rgb * vig * (1.0 + glow), color.a);\n }`\n };\n\n // \u2550\u2550\u2550\u2550\u2550\u2550\u2550 GEOMETRY \u2550\u2550\u2550\u2550\u2550\u2550\u2550\n const geometry = new THREE.BufferGeometry();\n const mainCapacity = Math.ceil(CONFIG.maxParticles * CONFIG.maxDensity);\n const pos = new Float32Array(mainCapacity * 3);\n const rnd = new Float32Array(mainCapacity * 3);\n const phase = new Float32Array(mainCapacity);\n const layer = new Float32Array(mainCapacity);\n const vec = new THREE.Vector3();\n\n for (let i = 0; i < mainCapacity; i++) {\n vec.setFromSphericalCoords(\n CONFIG.fieldRadius * Math.pow(Math.random(), 0.33),\n Math.acos(2 * Math.random() - 1),\n Math.random() * Math.PI * 2\n );\n pos[i * 3] = vec.x;\n pos[i * 3 + 1] = vec.y;\n pos[i * 3 + 2] = vec.z;\n rnd[i * 3] = Math.random();\n rnd[i * 3 + 1] = Math.random();\n rnd[i * 3 + 2] = Math.random();\n phase[i] = Math.random() * Math.PI * 2;\n }\n\n // Secondary particles now use separate instanced system below\n\n geometry.setAttribute('position', new THREE.BufferAttribute(pos, 3));\n geometry.setAttribute('aRandom', new THREE.BufferAttribute(rnd, 3));\n geometry.setAttribute('aPhase', new THREE.BufferAttribute(phase, 1));\n geometry.setAttribute('aLayer', new THREE.BufferAttribute(layer, 1));\n geometry.setDrawRange(0, Math.floor(CONFIG.maxParticles * STATE.density));\n\n const FIELD_TYPES = {\n 'Photon (EM)': { c1: '#00f2ff', c2: '#0051ff', c3: '#ffffff', s: 1.0, curl: 1.0, sz: 3.0, sc1: '#66d9ff', sc2: '#99eeff' },\n 'Gluon (Strong)': { c1: '#ff0055', c2: '#7000ff', c3: '#ff8800', s: 2.5, curl: 2.4, sz: 3.4, sc1: '#cc66ff', sc2: '#ff6699' },\n 'Higgs (Mass)': { c1: '#ffae00', c2: '#ff4800', c3: '#ffff00', s: 0.6, curl: 0.3, sz: 4.0, sc1: '#ffcc66', sc2: '#ffdd99' },\n 'Gravity': { c1: '#ffffff', c2: '#4466ff', c3: '#8844ff', s: 0.75, curl: 0.18, sz: 2.8, sc1: '#aabbff', sc2: '#ccaaff' },\n 'Dark Energy': { c1: '#1a0033', c2: '#6600ff', c3: '#ff00ff', s: 1.9, curl: 1.6, sz: 3.1, sc1: '#9933ff', sc2: '#cc66ff' },\n 'Neutrino': { c1: '#00ff88', c2: '#00ffcc', c3: '#88ffff', s: 3.0, curl: 3.0, sz: 2.6, sc1: '#66ffbb', sc2: '#99ffdd' },\n // === ENHANCEMENT: Waveform Terrain Field ===\n 'Waveform Terrain': { c1: '#ff3366', c2: '#33ff99', c3: '#6633ff', s: 0.4, curl: 0.15, sz: 2.4, sc1: '#ff6699', sc2: '#66ffcc', terrain: true }\n };\n\n // === ENHANCEMENT: Adaptive Quality System ===\n const QUALITY_SYSTEM = {\n frameCount: 0,\n frameTimes: [],\n lastAdjustTime: 0,\n currentFPS: 60,\n\n update(deltaTime) {\n this.frameCount++;\n this.frameTimes.push(deltaTime);\n if (this.frameTimes.length > 30) this.frameTimes.shift();\n\n if (this.frameCount % 30 === 0) {\n const avgDelta = this.frameTimes.reduce((a, b) => a + b, 0) / this.frameTimes.length;\n this.currentFPS = 1000 / avgDelta;\n\n if (STATE.adaptiveQualityEnabled && performance.now() - this.lastAdjustTime > 1000) {\n if (this.currentFPS < STATE.targetFPS - 10 && STATE.qualityLevel > 0.3) {\n STATE.qualityLevel = Math.max(0.3, STATE.qualityLevel - (this.currentFPS < STATE.targetFPS * 0.6 ? 0.2 : 0.1));\n this.applyQuality();\n this.lastAdjustTime = performance.now();\n } else if (this.currentFPS > STATE.targetFPS + 5 && STATE.qualityLevel < 1.0) {\n STATE.qualityLevel = Math.min(1.0, STATE.qualityLevel + 0.05);\n this.applyQuality();\n this.lastAdjustTime = performance.now();\n }\n }\n }\n },\n\n applyQuality() {\n const q = STATE.qualityLevel;\n const pixelRatio = Math.min(STATE.pixelRatio, Math.max(0.35, STATE.pixelRatio * q));\n const requestedMain = Math.floor(CONFIG.maxParticles * Math.min(STATE.density, CONFIG.maxDensity));\n const densityTotal = requestedMain + CONFIG.secondaryParticles;\n const requestedTotal = particleCount === null ? densityTotal : Math.min(particleCount, mainCapacity + CONFIG.secondaryParticles);\n const visibleParticles = Math.floor(Math.min(requestedTotal, particleBudget) * q);\n // Both layers keep the density's mix; whatever one layer cannot hold goes to the other.\n let mainCount = Math.min(mainCapacity, Math.round(visibleParticles * requestedMain / densityTotal));\n mainCount = Math.min(mainCapacity, Math.max(mainCount, visibleParticles - CONFIG.secondaryParticles));\n geometry.setDrawRange(0, mainCount);\n secondaryGeometry.instanceCount = visibleParticles - mainCount;\n if (renderer.getPixelRatio() !== pixelRatio) {\n renderer.setPixelRatio(pixelRatio);\n composer.setPixelRatio(pixelRatio);\n }\n material.uniforms.uPixelRatio.value = pixelRatio;\n secondaryMaterial.uniforms.uPixelRatio.value = pixelRatio;\n }\n };\n\n // === ENHANCEMENT: Audio-Reactive Camera System ===\n const AUDIO_CAMERA = {\n targetOffset: new THREE.Vector3(),\n currentOffset: new THREE.Vector3(),\n targetFOV: 55,\n baseFOV: 55,\n breathPhase: 0,\n\n update(deltaTime) {\n if (!STATE.audioCameraEnabled || !(AUDIO.active || AUDIO.externalActive)) return;\n\n const bass = AUDIO.gatedBands.bass;\n const mid = AUDIO.gatedBands.mid;\n const high = AUDIO.gatedBands.high;\n const intensity = STATE.audioCameraIntensity;\n const smooth = STATE.audioCameraSmoothing;\n\n // Bass-driven zoom pulse\n this.targetFOV = this.baseFOV - bass * 8 * intensity + AUDIO.beatEnergy * 5 * intensity;\n camera.fov += (this.targetFOV - camera.fov) * smooth;\n camera.updateProjectionMatrix();\n\n // Subtle sway based on mid frequencies\n this.breathPhase += deltaTime * 0.001 * (1 + mid * 2);\n this.targetOffset.x = Math.sin(this.breathPhase * 0.7) * mid * 3 * intensity;\n this.targetOffset.y = Math.cos(this.breathPhase * 0.5) * high * 2 * intensity;\n this.targetOffset.z = Math.sin(this.breathPhase * 0.3) * bass * 4 * intensity;\n\n // Smooth interpolation\n this.currentOffset.lerp(this.targetOffset, smooth);\n\n // Apply onset punch\n if (AUDIO.onsetDetected) {\n this.currentOffset.z -= AUDIO.onsetEnergy * 2 * intensity;\n }\n }\n };\n\n const def = FIELD_TYPES[STATE.field];\n const material = new THREE.ShaderMaterial({\n vertexShader, fragmentShader,\n uniforms: {\n uTime: { value: 0 },\n uPixelRatio: { value: STATE.pixelRatio },\n uSizeBase: { value: def.sz },\n uNoiseScale: { value: def.s },\n uCurlStrength: { value: def.curl },\n uViewportAspect: { value: window.innerWidth / window.innerHeight },\n uViewportHalfHeight: { value: Math.tan(camera.fov * Math.PI / 360) * CONFIG.defaultZoom },\n uFieldFill: { value: STATE.fieldSize },\n uColor1: { value: new THREE.Color(def.c1) },\n uColor2: { value: new THREE.Color(def.c2) },\n uColor3: { value: new THREE.Color(def.c3) },\n uSubBass: { value: 0 }, uBass: { value: 0 }, uLowMid: { value: 0 },\n uMid: { value: 0 }, uHighMid: { value: 0 }, uHigh: { value: 0 }, uUltraHigh: { value: 0 },\n uBeatEnergy: { value: 0 }, uSpectralCentroid: { value: 0 }, uSpectralFlux: { value: 0 },\n uOnsetEnergy: { value: 0 }, uTerrainMode: { value: 0 }, uTerrainHeight: { value: 15.0 },\n uVortexStrength: { value: STATE.vortexStrength },\n uPulseIntensity: { value: STATE.pulseIntensity },\n uZoomFactor: { value: 1.0 },\n uMousePos: { value: new THREE.Vector3(999, 999, 999) },\n uMouseVelocity: { value: new THREE.Vector2(0, 0) }\n },\n transparent: true, depthWrite: false, blending: THREE.AdditiveBlending\n });\n\n const points = new THREE.Points(geometry, material);\n scene.add(points);\n\n // \u2550\u2550\u2550\u2550\u2550\u2550\u2550 INSTANCED SECONDARY PARTICLES \u2550\u2550\u2550\u2550\u2550\u2550\u2550\n const secondaryVertexShader = `\nuniform float uTime, uPixelRatio;\nuniform float uViewportAspect, uViewportHalfHeight;\nuniform float uSubBass, uBass, uLowMid, uMid, uHighMid, uHigh, uUltraHigh;\nuniform float uBeatEnergy, uSpectralCentroid, uSpectralFlux, uOnsetEnergy;\nuniform vec3 uColor1, uColor2;\nuniform float uVortexStrength;\n\nattribute vec3 instancePosition;\nattribute vec3 instanceRandom;\nattribute float instancePhase;\n\nvarying vec3 vColor;\nvarying float vAlpha;\nvarying float vEnergy;\nvarying float vDepth;\n\nvec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }\nvec4 mod289(vec4 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }\nvec4 permute(vec4 x) { return mod289(((x*34.0)+1.0)*x); }\nvec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; }\n\nfloat snoise(vec3 v) {\n const vec2 C = vec2(1.0/6.0, 1.0/3.0);\n const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);\n vec3 i = floor(v + dot(v, C.yyy));\n vec3 x0 = v - i + dot(i, C.xxx);\n vec3 g = step(x0.yzx, x0.xyz);\n vec3 l = 1.0 - g;\n vec3 i1 = min(g.xyz, l.zxy);\n vec3 i2 = max(g.xyz, l.zxy);\n vec3 x1 = x0 - i1 + C.xxx;\n vec3 x2 = x0 - i2 + C.yyy;\n vec3 x3 = x0 - D.yyy;\n i = mod289(i);\n 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));\n float n_ = 0.142857142857;\n vec3 ns = n_ * D.wyz - D.xzx;\n vec4 j = p - 49.0 * floor(p * ns.z * ns.z);\n vec4 x_ = floor(j * ns.z);\n vec4 y_ = floor(j - 7.0 * x_);\n vec4 x = x_ * ns.x + ns.yyyy;\n vec4 y = y_ * ns.x + ns.yyyy;\n vec4 h = 1.0 - abs(x) - abs(y);\n vec4 b0 = vec4(x.xy, y.xy);\n vec4 b1 = vec4(x.zw, y.zw);\n vec4 s0 = floor(b0) * 2.0 + 1.0;\n vec4 s1 = floor(b1) * 2.0 + 1.0;\n vec4 sh = -step(h, vec4(0.0));\n vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy;\n vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww;\n vec3 p0 = vec3(a0.xy, h.x);\n vec3 p1 = vec3(a0.zw, h.y);\n vec3 p2 = vec3(a1.xy, h.z);\n vec3 p3 = vec3(a1.zw, h.w);\n vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2,p2), dot(p3,p3)));\n p0 *= norm.x; p1 *= norm.y; p2 *= norm.z; p3 *= norm.w;\n vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);\n m = m * m;\n return 42.0 * dot(m*m, vec4(dot(p0,x0), dot(p1,x1), dot(p2,x2), dot(p3,x3)));\n}\n\n${shapeFunction}\n\nvoid main() {\n // Secondary particles run at a slower timescale \u2014 they are the atmosphere, not the core\n float t = uTime * 0.45;\n\n // Multi-scale noise flow \u2014 low freqs drive large drift, high freqs add fine turbulence\n vec3 noisePos = instancePosition * 0.035 + instanceRandom;\n vec3 drift = vec3(\n snoise(noisePos + t * 0.06),\n snoise(noisePos * 0.9 - t * 0.05),\n snoise(noisePos * 1.1 + t * 0.04)\n ) * (5.0 + uSubBass * 10.0 + uLowMid * 6.0);\n\n vec3 turbulence = vec3(\n snoise(noisePos * 2.5 + t * 0.18),\n snoise(noisePos * 2.5 - t * 0.14),\n snoise(noisePos * 2.5 + t * 0.12)\n ) * (uHigh * 5.0 + uSpectralFlux * 8.0);\n\n vec3 offset = drift + turbulence;\n\n // Orbital drift \u2014 vortex-aware, modulated by bass\n float orbitSpeed = 0.12 + instanceRandom.x * 0.15 + uBass * 0.08;\n float angle = uTime * orbitSpeed + instancePhase;\n float orbitRadius = length(instancePosition.xz) * (1.0 + uSubBass * 0.25);\n // Vortex coupling \u2014 secondary shell rotates with the main field vortex\n float vortexInfluence = uVortexStrength * uBass * 0.4;\n float vAngle = angle + vortexInfluence;\n vec3 orbit = vec3(\n cos(vAngle) * orbitRadius - instancePosition.x,\n sin(uTime * 0.5 + instancePhase) * (uMid * 4.0 + uLowMid * 2.0),\n sin(vAngle) * orbitRadius - instancePosition.z\n ) * 0.07;\n\n vec3 newPos = shapedPosition(instanceRandom) + offset + orbit;\n\n // Transients and sub-bass move the atmosphere in depth, not away from the centre.\n newPos.z += uOnsetEnergy * 14.0 * (0.6 + instanceRandom.z * 0.4);\n newPos.z += sin(uTime * 1.2 + instancePhase) * uSubBass * 5.0;\n\n vec4 mvPosition = modelViewMatrix * vec4(newPos, 1.0);\n gl_Position = projectionMatrix * mvPosition;\n\n // Size: driven by different bands than the main system\n // Sparkle on ultraHigh, swell on subBass, punch on beatEnergy\n float sparkle = 1.0 + (instanceRandom.x > 0.85 ? sin(uTime * 15.0 + instanceRandom.y * 200.0) * uUltraHigh * 3.0 : 0.0);\n float swell = 1.0 + uSubBass * 0.6 + uLowMid * 0.3;\n float size = (1.1 + uBeatEnergy * 0.7) * swell * sparkle;\n gl_PointSize = clamp(size * uPixelRatio * (55.0 / -mvPosition.z), 0.5, 55.0);\n\n // Colour: spectral centroid shifts hue, highMid brightens toward white\n float centroidMix = clamp(uSpectralCentroid * 2.2 + uHighMid * 0.4, 0.0, 1.0);\n vColor = mix(uColor1, uColor2, centroidMix);\n // SubBass warmth \u2014 hint of deep colour saturation on heavy lows\n vColor = mix(vColor, vColor * 1.3, uSubBass * 0.4);\n // Beat flash \u2014 brief white-hot core\n vColor = mix(vColor, vec3(1.0), uBeatEnergy * 0.25);\n\n // Alpha: outer shell fades at edge, mid-frequency presence sustains visibility\n float edgeFade = 1.0 - smoothstep(0.92, 1.05, max(abs(newPos.x) / (uViewportAspect * uViewportHalfHeight * uFieldFill * 1.05), abs(newPos.y) / (uViewportHalfHeight * uFieldFill * 1.05)));\n vAlpha = edgeFade * (0.45 + uMid * 0.35 + uLowMid * 0.2) * sparkle;\n\n vEnergy = uBeatEnergy * 0.4 + uHighMid * 0.3 + uOnsetEnergy * 0.3;\n vDepth = clamp(-mvPosition.z / 160.0, 0.0, 1.0);\n}\n`;\n\n const secondaryFragmentShader = `\nvarying vec3 vColor;\nvarying float vAlpha;\nvarying float vEnergy;\nvarying float vDepth;\n\nvoid main() {\n vec2 xy = gl_PointCoord.xy - vec2(0.5);\n float r = length(xy);\n if (r > 0.5) discard;\n\n // Wider, softer glow than main particles \u2014 atmospheric haze\n float glow = exp(-r * r * 8.0);\n float softGlow = exp(-r * r * 3.0) * 0.4;\n float core = smoothstep(0.18 - vEnergy * 0.06, 0.10, r);\n\n vec3 finalColor = mix(vColor, vec3(1.0, 0.97, 0.92), core * 0.55);\n // Depth haze \u2014 far particles shift slightly cooler/dimmer\n finalColor = mix(finalColor, vec3(0.02, 0.03, 0.06), vDepth * 0.35);\n\n float finalAlpha = clamp(vAlpha * (glow * 0.65 + softGlow + core * 0.35), 0.0, 1.0);\n gl_FragColor = vec4(finalColor, finalAlpha);\n}\n`;\n\n // Create instanced geometry for secondary particles\n const secondaryGeometry = new THREE.InstancedBufferGeometry();\n\n // Base geometry (single point)\n const basePositions = new Float32Array([0, 0, 0]);\n secondaryGeometry.setAttribute('position', new THREE.BufferAttribute(basePositions, 3));\n\n // Instance attributes\n const instancePositions = new Float32Array(CONFIG.secondaryParticles * 3);\n const instanceRandoms = new Float32Array(CONFIG.secondaryParticles * 3);\n const instancePhases = new Float32Array(CONFIG.secondaryParticles);\n\n for (let i = 0; i < CONFIG.secondaryParticles; i++) {\n // Distribute in outer shell\n const r = CONFIG.fieldRadius * (0.65 + Math.random() * 0.45);\n const theta = Math.acos(2 * Math.random() - 1);\n const phi = Math.random() * Math.PI * 2;\n\n instancePositions[i * 3] = r * Math.sin(theta) * Math.cos(phi);\n instancePositions[i * 3 + 1] = r * Math.sin(theta) * Math.sin(phi);\n instancePositions[i * 3 + 2] = r * Math.cos(theta);\n\n instanceRandoms[i * 3] = Math.random();\n instanceRandoms[i * 3 + 1] = Math.random();\n instanceRandoms[i * 3 + 2] = Math.random();\n\n instancePhases[i] = Math.random() * Math.PI * 2;\n }\n\n secondaryGeometry.setAttribute('instancePosition', new THREE.InstancedBufferAttribute(instancePositions, 3));\n secondaryGeometry.setAttribute('instanceRandom', new THREE.InstancedBufferAttribute(instanceRandoms, 3));\n secondaryGeometry.setAttribute('instancePhase', new THREE.InstancedBufferAttribute(instancePhases, 1));\n\n const secondaryMaterial = new THREE.ShaderMaterial({\n vertexShader: secondaryVertexShader,\n fragmentShader: secondaryFragmentShader,\n uniforms: {\n uTime: { value: 0 },\n uPixelRatio: { value: STATE.pixelRatio },\n uSubBass: { value: 0 },\n uBass: { value: 0 },\n uLowMid: { value: 0 },\n uMid: { value: 0 },\n uHighMid: { value: 0 },\n uHigh: { value: 0 },\n uUltraHigh: { value: 0 },\n uBeatEnergy: { value: 0 },\n uSpectralCentroid: { value: 0 },\n uSpectralFlux: { value: 0 },\n uOnsetEnergy: { value: 0 },\n uVortexStrength: { value: STATE.vortexStrength },\n uColor1: { value: new THREE.Color(def.sc1) },\n uColor2: { value: new THREE.Color(def.sc2) },\n uViewportAspect: { value: window.innerWidth / window.innerHeight },\n uViewportHalfHeight: { value: Math.tan(camera.fov * Math.PI / 360) * CONFIG.defaultZoom },\n uFieldFill: { value: STATE.fieldSize }\n },\n transparent: true,\n depthWrite: false,\n blending: THREE.AdditiveBlending\n });\n\n const secondaryPoints = new THREE.Points(secondaryGeometry, secondaryMaterial);\n scene.add(secondaryPoints);\n\n // Expose secondary-layer controls to the host component.\n STATE.secondaryEnabled = true;\n STATE.secondaryOpacity = 1.0;\n\n // \u2550\u2550\u2550\u2550\u2550\u2550\u2550 PARTICLE TRAIL SYSTEM \u2550\u2550\u2550\u2550\u2550\u2550\u2550\n const particleSeeds = geometry.getAttribute('aRandom');\n function fieldParticlePosition(index, target) {\n const halfHeight = material.uniforms.uViewportHalfHeight.value * STATE.fieldSize;\n const halfWidth = halfHeight * material.uniforms.uViewportAspect.value;\n return target.set(\n (particleSeeds.getX(index) * 2 - 1) * halfWidth,\n (particleSeeds.getY(index) * 2 - 1) * halfHeight,\n (particleSeeds.getZ(index) - 0.5) * 12\n );\n }\n\n class TrailSystem {\n constructor(count, length) {\n this.trails = [];\n this.meshes = [];\n this.nextPosition = new THREE.Vector3();\n\n for (let i = 0; i < count; i++) {\n const anchor = new THREE.Vector2(Math.random() * 1.5 - 0.75, Math.random() * 1.5 - 0.75);\n const start = new THREE.Vector3(\n anchor.x * material.uniforms.uViewportAspect.value * material.uniforms.uViewportHalfHeight.value * STATE.fieldSize,\n anchor.y * material.uniforms.uViewportHalfHeight.value * STATE.fieldSize,\n 0\n );\n const trail = {\n positions: Array.from({ length }, () => start.clone()),\n anchor,\n phase: Math.random() * Math.PI * 2,\n speed: 0.3 + Math.random() * 0.7,\n color: new THREE.Color(def.c1).lerp(new THREE.Color(def.c2), i / Math.max(1, count - 1))\n };\n this.trails.push(trail);\n\n const geom = new THREE.BufferGeometry();\n const positions = new Float32Array(length * 3);\n const alphas = new Float32Array(length);\n for (let j = 0; j < length; j++) alphas[j] = 1.0 - j / length;\n\n geom.setAttribute('position', new THREE.BufferAttribute(positions, 3));\n geom.setAttribute('alpha', new THREE.BufferAttribute(alphas, 1));\n\n const mat = new THREE.ShaderMaterial({\n vertexShader: `attribute float alpha; varying float vAlpha; void main() { vAlpha = alpha; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`,\n 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)); }`,\n uniforms: { uColor: { value: trail.color }, uOpacity: { value: 0.4 }, uBeat: { value: 0 } },\n transparent: true, depthWrite: false, blending: THREE.AdditiveBlending\n });\n\n const line = new THREE.Line(geom, mat);\n this.meshes.push(line);\n scene.add(line);\n }\n }\n\n update(time, audio, fieldColor) {\n const halfHeight = material.uniforms.uViewportHalfHeight.value * STATE.fieldSize;\n const halfWidth = halfHeight * material.uniforms.uViewportAspect.value;\n for (let i = 0; i < this.trails.length; i++) {\n const trail = this.trails[i], mesh = this.meshes[i];\n mesh.visible = STATE.particleTrailsEnabled && i < this.trails.length * STATE.qualityLevel;\n if (!mesh.visible) continue;\n\n const angle = time * 0.5 * trail.speed + trail.phase;\n const newPos = this.nextPosition.set(\n trail.anchor.x * halfWidth + Math.cos(angle) * (4 + audio.bass * 8),\n trail.anchor.y * halfHeight + Math.sin(time * 0.3 + trail.phase) * (4 + audio.mid * 8),\n Math.sin(angle) * (4 + audio.high * 7)\n );\n newPos.x = THREE.MathUtils.clamp(newPos.x, -halfWidth * 0.94, halfWidth * 0.94);\n newPos.y = THREE.MathUtils.clamp(newPos.y, -halfHeight * 0.94, halfHeight * 0.94);\n\n for (let j = trail.positions.length - 1; j > 0; j--) trail.positions[j].copy(trail.positions[j - 1]);\n trail.positions[0].copy(newPos);\n\n const posAttr = mesh.geometry.getAttribute('position');\n for (let j = 0; j < trail.positions.length; j++) {\n posAttr.setXYZ(j, trail.positions[j].x, trail.positions[j].y, trail.positions[j].z);\n }\n posAttr.needsUpdate = true;\n\n mesh.material.uniforms.uBeat.value = audio.beat;\n mesh.material.uniforms.uOpacity.value = STATE.particleTrailOpacity;\n }\n }\n }\n\n const trailSystem = new TrailSystem(50, 12);\n\n // \u2550\u2550\u2550\u2550\u2550\u2550\u2550 PARTICLE CONNECTIONS (Force Network) \u2550\u2550\u2550\u2550\u2550\u2550\u2550\n const connectionCount = 500; // Number of particles to check for connections\n const maxConnections = 800; // Maximum line segments\n const connectionGeometry = new THREE.BufferGeometry();\n const connectionPositions = new Float32Array(maxConnections * 6); // 2 points per line, 3 coords each\n const connectionAlphas = new Float32Array(maxConnections * 2); // Alpha per vertex\n connectionGeometry.setAttribute('position', new THREE.BufferAttribute(connectionPositions, 3));\n connectionGeometry.setAttribute('alpha', new THREE.BufferAttribute(connectionAlphas, 1));\n connectionGeometry.setDrawRange(0, 0);\n\n const connectionMaterial = new THREE.ShaderMaterial({\n vertexShader: `\n attribute float alpha;\n varying float vAlpha;\n void main() {\n vAlpha = alpha;\n vec4 mvPos = modelViewMatrix * vec4(position, 1.0);\n gl_Position = projectionMatrix * mvPos;\n }\n `,\n fragmentShader: `\n uniform vec3 uColor;\n uniform vec3 uColor2;\n uniform float uOpacity;\n uniform float uBeatEnergy;\n uniform float uHighMid;\n uniform float uSpectralCentroid;\n varying float vAlpha;\n void main() {\n // Blend hue with spectral centroid (brighter/higher = more c2)\n vec3 color = mix(uColor, uColor2, clamp(uSpectralCentroid * 2.0, 0.0, 1.0));\n // Beat pulse flash\n float beatFlash = 1.0 + uBeatEnergy * 1.2;\n color *= beatFlash;\n // Per-line alpha reacts to highMid (attack/presence energy)\n float lineAlpha = vAlpha * uOpacity * (0.4 + uHighMid * 0.5 + uBeatEnergy * 0.4);\n gl_FragColor = vec4(color, clamp(lineAlpha, 0.0, 1.0));\n }\n `,\n uniforms: {\n uColor: { value: new THREE.Color(def.c2) },\n uColor2: { value: new THREE.Color(def.c1) },\n uOpacity: { value: 1.0 },\n uBeatEnergy: { value: 0 },\n uHighMid: { value: 0 },\n uSpectralCentroid: { value: 0 }\n },\n transparent: true,\n depthWrite: false,\n blending: THREE.AdditiveBlending\n });\n\n const connectionMesh = new THREE.LineSegments(connectionGeometry, connectionMaterial);\n scene.add(connectionMesh);\n\n // Particle indices to track for connections \u2014 refreshed periodically\n const trackedParticles = [];\n const positions = Array.from({ length: connectionCount }, () => new THREE.Vector3());\n let connectionRefreshCounter = 0;\n const CONNECTION_REFRESH_INTERVAL = 40; // 20 Hz updates preserve the original ~2 s selection cycle\n\n function refreshTrackedParticles() {\n trackedParticles.length = 0;\n const visibleParticles = Math.max(1, geometry.drawRange.count);\n const visibleFraction = Math.sqrt(visibleParticles / (CONFIG.maxParticles * CONFIG.initialDensity));\n const trackedCount = Math.max(2, Math.floor(connectionCount * Math.min(1, STATE.qualityLevel, visibleFraction)));\n for (let i = 0; i < trackedCount; i++) {\n trackedParticles.push(Math.floor(Math.random() * visibleParticles));\n }\n }\n refreshTrackedParticles();\n let lastConnectionState = null;\n\n function shouldRebuildConnections(activeAudio, current, previous, idleElapsed) {\n return activeAudio || idleElapsed >= 2 || !previous || current.some((value, index) => value !== previous[index]);\n }\n\n function updateConnections(threshold, bassBoost, highMid, mid, forceRefresh = false) {\n // Refresh particle selection every N frames \u2014 network evolves over time\n connectionRefreshCounter++;\n if (forceRefresh || connectionRefreshCounter >= CONNECTION_REFRESH_INTERVAL) {\n refreshTrackedParticles();\n connectionRefreshCounter = 0;\n }\n\n // Mirror the shader's screen-fitted seed mapping for the line systems.\n for (let i = 0; i < trackedParticles.length; i++) {\n fieldParticlePosition(trackedParticles[i], positions[i]);\n }\n\n // Threshold breathes with bass, tightens on high energy\n const effectiveThreshold = threshold * (1.0 + bassBoost * 0.6) * (1.0 - highMid * 0.15);\n const thresholdSq = effectiveThreshold * effectiveThreshold;\n\n // Dynamic connection cap \u2014 expands with mid+highMid energy\n const dynamicMax = Math.floor(maxConnections * (0.3 + (mid + highMid) * 0.7));\n\n let connIdx = 0;\n for (let i = 0; i < trackedParticles.length && connIdx < dynamicMax; i++) {\n for (let j = i + 1; j < trackedParticles.length && connIdx < dynamicMax; j++) {\n const distSq = positions[i].distanceToSquared(positions[j]);\n if (distSq < thresholdSq && distSq > 0.1) {\n const dist = Math.sqrt(distSq);\n const alpha = (1.0 - dist / effectiveThreshold) * 0.8;\n\n connectionPositions[connIdx * 6 + 0] = positions[i].x;\n connectionPositions[connIdx * 6 + 1] = positions[i].y;\n connectionPositions[connIdx * 6 + 2] = positions[i].z;\n connectionPositions[connIdx * 6 + 3] = positions[j].x;\n connectionPositions[connIdx * 6 + 4] = positions[j].y;\n connectionPositions[connIdx * 6 + 5] = positions[j].z;\n\n connectionAlphas[connIdx * 2 + 0] = alpha;\n connectionAlphas[connIdx * 2 + 1] = alpha;\n\n connIdx++;\n }\n }\n }\n\n connectionGeometry.attributes.position.needsUpdate = true;\n connectionGeometry.attributes.alpha.needsUpdate = true;\n connectionGeometry.setDrawRange(0, connIdx * 2);\n }\n\n // \u2550\u2550\u2550\u2550\u2550\u2550\u2550 NETWORK CRAWLERS \u2550\u2550\u2550\u2550\u2550\u2550\u2550\n const CRAWLER_COUNT = 24; // concurrent crawlers\n const CRAWLER_TAIL = 10; // trail positions per crawler\n const crawlerMaxSegs = CRAWLER_COUNT * (CRAWLER_TAIL - 1);\n\n const crawlerGeo = new THREE.BufferGeometry();\n const crawlerPos = new Float32Array(crawlerMaxSegs * 6);\n const crawlerAlp = new Float32Array(crawlerMaxSegs * 2);\n crawlerGeo.setAttribute('position', new THREE.BufferAttribute(crawlerPos, 3));\n crawlerGeo.setAttribute('alpha', new THREE.BufferAttribute(crawlerAlp, 1));\n crawlerGeo.setDrawRange(0, 0);\n\n // Bloom only lights pixels above 0.85 luminance. Preset greens and cyans got there on their own;\n // a theme's reds and violets carry the same energy at a fraction of the luminance, so reaction\n // colours keep the theme's hue but are lifted (past 1.0 if needed) to the brightness of the originals.\n function glowColor(color, luminance) {\n const current = 0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b;\n return color.multiplyScalar(Math.min(4, Math.max(1, luminance / Math.max(current, 0.02))));\n }\n const crawlerLow = new THREE.Color(def.c1), crawlerHigh = new THREE.Color(def.c3);\n const crawlerTarget = new THREE.Color();\n const crawlerMaterial = new THREE.ShaderMaterial({\n vertexShader: `\n attribute float alpha;\n varying float vAlpha;\n void main() {\n vAlpha = alpha;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n }\n `,\n fragmentShader: `\n uniform vec3 uColor;\n uniform float uBeatEnergy;\n varying float vAlpha;\n void main() {\n vec3 c = uColor * (1.2 + uBeatEnergy * 1.5);\n gl_FragColor = vec4(c, vAlpha);\n }\n `,\n uniforms: {\n uColor: { value: new THREE.Color(def.c1) },\n uBeatEnergy: { value: 0 }\n },\n transparent: true,\n depthWrite: false,\n blending: THREE.AdditiveBlending\n });\n\n const crawlerMesh = new THREE.LineSegments(crawlerGeo, crawlerMaterial);\n scene.add(crawlerMesh);\n\n // Initialise crawler agents\n const crawlers = [];\n const CRAWLER_MIN_STEP = 1.8; // min world-space distance before adding a tail point\n for (let i = 0; i < CRAWLER_COUNT; i++) {\n crawlers.push({\n fromIdx: Math.floor(Math.random() * connectionCount),\n toIdx: Math.floor(Math.random() * connectionCount),\n t: Math.random(),\n speed: 0.0015 + Math.random() * 0.002,\n tail: [],\n lastTailPos: null, // last world position a tail point was recorded\n hue: Math.random()\n });\n }\n\n function updateCrawlers(dt, highMid, beatEnergy, spectralCentroid) {\n // Helper: world position of a tracked particle\n function particlePos(idx) {\n return positions[idx % trackedParticles.length];\n }\n\n // Helper: find nearest neighbour within threshold\n function nearestNeighbour(fromIdx, excludeIdx) {\n const origin = particlePos(fromIdx);\n const threshold = STATE.connectionThreshold * (1.2 + highMid * 0.4);\n const threshSq = threshold * threshold;\n let bestDist = Infinity, bestIdx = -1;\n const check = Math.min(trackedParticles.length, 80); // sample subset for perf\n const start = Math.floor(Math.random() * (trackedParticles.length - check));\n for (let k = start; k < start + check; k++) {\n const ki = k % trackedParticles.length;\n if (ki === fromIdx || ki === excludeIdx) continue;\n const d = origin.distanceToSquared(particlePos(ki));\n if (d < threshSq && d < bestDist) { bestDist = d; bestIdx = ki; }\n }\n // fallback: pick random if no neighbour in range\n return bestIdx === -1 ? Math.floor(Math.random() * trackedParticles.length) : bestIdx;\n }\n\n let segIdx = 0;\n\n for (let i = 0; i < crawlers.length; i++) {\n const c = crawlers[i];\n\n // Speed reacts to highMid (attack energy) and beat\n const spd = c.speed * (1.0 + highMid * 0.8 + beatEnergy * 0.5);\n c.t += spd;\n\n // Reached target \u2014 advance to next node\n if (c.t >= 1.0) {\n c.t -= 1.0;\n c.fromIdx = c.toIdx;\n c.toIdx = nearestNeighbour(c.fromIdx, c.fromIdx);\n // Occasionally teleport to keep crawlers spread across the field\n if (Math.random() < 0.04) {\n c.fromIdx = Math.floor(Math.random() * trackedParticles.length);\n c.toIdx = nearestNeighbour(c.fromIdx, c.fromIdx);\n c.tail = [];\n c.lastTailPos = null;\n }\n }\n\n // Current head position\n const from = particlePos(c.fromIdx);\n const to = particlePos(c.toIdx);\n const head = new THREE.Vector3().lerpVectors(from, to, c.t);\n\n // Only record a tail point when head has moved far enough in world space\n const distMoved = c.lastTailPos ? head.distanceTo(c.lastTailPos) : Infinity;\n if (distMoved >= CRAWLER_MIN_STEP) {\n c.tail.push(head.clone());\n if (c.tail.length > CRAWLER_TAIL) c.tail.shift();\n c.lastTailPos = head.clone();\n }\n\n // Write tail segments\n const tailLen = c.tail.length;\n for (let s = 0; s < tailLen - 1 && segIdx < crawlerMaxSegs; s++) {\n const p0 = c.tail[s];\n const p1 = c.tail[s + 1];\n // Alpha: head is brightest, tail fades\n const baseAlpha = ((s + 1) / (tailLen - 1));\n const alpha = baseAlpha * (0.25 + spectralCentroid * 0.3 + beatEnergy * 0.4);\n\n crawlerPos[segIdx * 6 + 0] = p0.x; crawlerPos[segIdx * 6 + 1] = p0.y; crawlerPos[segIdx * 6 + 2] = p0.z;\n crawlerPos[segIdx * 6 + 3] = p1.x; crawlerPos[segIdx * 6 + 4] = p1.y; crawlerPos[segIdx * 6 + 5] = p1.z;\n crawlerAlp[segIdx * 2 + 0] = alpha;\n crawlerAlp[segIdx * 2 + 1] = alpha;\n segIdx++;\n }\n }\n\n crawlerGeo.attributes.position.needsUpdate = true;\n crawlerGeo.attributes.alpha.needsUpdate = true;\n crawlerGeo.setDrawRange(0, segIdx * 2);\n }\n\n STATE.crawlersEnabled = true;\n\n // \u2550\u2550\u2550\u2550\u2550\u2550\u2550 NEBULA BACKGROUND \u2550\u2550\u2550\u2550\u2550\u2550\u2550\n const nebulaVertexShader = `\nvarying vec3 vWorldPosition;\nvarying vec2 vUv;\nvoid main() {\n vUv = uv;\n vec4 worldPos = modelMatrix * vec4(position, 1.0);\n vWorldPosition = worldPos.xyz;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n}\n`;\n\n const nebulaFragmentShader = `\nuniform float uTime;\nuniform vec3 uColor1;\nuniform vec3 uColor2;\nuniform vec3 uColor3;\nuniform float uBass;\nuniform float uMid;\nuniform float uHigh;\nuniform float uBeatEnergy;\nuniform float uIntensity;\nvarying vec3 vWorldPosition;\nvarying vec2 vUv;\n\n// Simplex 3D noise\nvec4 permute(vec4 x) { return mod(((x*34.0)+1.0)*x, 289.0); }\nvec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; }\n\nfloat snoise(vec3 v) {\n const vec2 C = vec2(1.0/6.0, 1.0/3.0);\n const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);\n vec3 i = floor(v + dot(v, C.yyy));\n vec3 x0 = v - i + dot(i, C.xxx);\n vec3 g = step(x0.yzx, x0.xyz);\n vec3 l = 1.0 - g;\n vec3 i1 = min(g.xyz, l.zxy);\n vec3 i2 = max(g.xyz, l.zxy);\n vec3 x1 = x0 - i1 + C.xxx;\n vec3 x2 = x0 - i2 + C.yyy;\n vec3 x3 = x0 - D.yyy;\n i = mod(i, 289.0);\n 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));\n float n_ = 1.0/7.0;\n vec3 ns = n_ * D.wyz - D.xzx;\n vec4 j = p - 49.0 * floor(p * ns.z * ns.z);\n vec4 x_ = floor(j * ns.z);\n vec4 y_ = floor(j - 7.0 * x_);\n vec4 x = x_ *ns.x + ns.yyyy;\n vec4 y = y_ *ns.x + ns.yyyy;\n vec4 h = 1.0 - abs(x) - abs(y);\n vec4 b0 = vec4(x.xy, y.xy);\n vec4 b1 = vec4(x.zw, y.zw);\n vec4 s0 = floor(b0)*2.0 + 1.0;\n vec4 s1 = floor(b1)*2.0 + 1.0;\n vec4 sh = -step(h, vec4(0.0));\n vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy;\n vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww;\n vec3 p0 = vec3(a0.xy, h.x);\n vec3 p1 = vec3(a0.zw, h.y);\n vec3 p2 = vec3(a1.xy, h.z);\n vec3 p3 = vec3(a1.zw, h.w);\n vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2,p2), dot(p3,p3)));\n p0 *= norm.x; p1 *= norm.y; p2 *= norm.z; p3 *= norm.w;\n vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);\n return 42.0 * dot(m*m*m*m, vec4(dot(p0,x0), dot(p1,x1), dot(p2,x2), dot(p3,x3)));\n}\n\nfloat fbm(vec3 p) {\n float value = 0.0;\n float amplitude = 0.5;\n float frequency = 1.0;\n for(int i = 0; i < 3; i++) {\n value += amplitude * snoise(p * frequency);\n amplitude *= 0.5;\n frequency *= 2.0;\n }\n return value;\n}\n\nvoid main() {\n vec3 dir = normalize(vWorldPosition);\n float t = uTime * 0.02;\n\n // Layered nebula clouds\n vec3 p1 = dir * 2.0 + t * 0.5;\n vec3 p2 = dir * 4.0 - t * 0.3;\n vec3 p3 = dir * 1.5 + vec3(t * 0.2, -t * 0.1, t * 0.15);\n\n float n1 = fbm(p1) * 0.5 + 0.5;\n float n2 = fbm(p2) * 0.5 + 0.5;\n float n3 = fbm(p3) * 0.5 + 0.5;\n\n // Create cloud density\n float density = n1 * n2;\n density = pow(density, 1.5) * 1.5;\n\n // Wispy tendrils\n float tendrils = pow(n3, 3.0) * 0.8;\n\n // Color mixing based on field colors\n vec3 nebulaColor = mix(uColor1 * 0.3, uColor2 * 0.5, n1);\n nebulaColor = mix(nebulaColor, uColor3 * 0.4, n2 * 0.5);\n\n // Add some complementary deep space colors\n vec3 deepSpace = vec3(0.02, 0.01, 0.05);\n vec3 starGlow = vec3(0.1, 0.08, 0.15);\n\n // Audio reactivity\n float bassGlow = uBass * 0.3;\n float midShimmer = uMid * 0.2 * sin(uTime * 2.0 + n1 * 10.0);\n float beatPulse = uBeatEnergy * 0.4;\n\n // Combine layers\n vec3 color = deepSpace;\n color += nebulaColor * density * (0.4 + bassGlow + beatPulse);\n color += uColor2 * 0.15 * tendrils * (1.0 + uHigh * 0.5);\n color += starGlow * pow(n1 * n2, 4.0) * (1.0 + midShimmer);\n\n // Scattered stars\n float stars = pow(snoise(dir * 50.0) * 0.5 + 0.5, 20.0);\n stars += pow(snoise(dir * 80.0 + 100.0) * 0.5 + 0.5, 25.0) * 0.5;\n color += vec3(1.0, 0.95, 0.9) * stars * (0.5 + uHigh * 0.5);\n\n // Vignette toward horizon\n float horizonFade = 1.0 - pow(abs(dir.y), 0.5) * 0.3;\n color *= horizonFade;\n\n // Apply intensity\n color *= uIntensity;\n\n gl_FragColor = vec4(color, 1.0);\n}\n`;\n\n const nebulaMaterial = new THREE.ShaderMaterial({\n vertexShader: nebulaVertexShader,\n fragmentShader: nebulaFragmentShader,\n uniforms: {\n uTime: { value: 0 },\n uColor1: { value: new THREE.Color(def.c1) },\n uColor2: { value: new THREE.Color(def.c2) },\n uColor3: { value: new THREE.Color(def.c3) },\n uBass: { value: 0 },\n uMid: { value: 0 },\n uHigh: { value: 0 },\n uBeatEnergy: { value: 0 },\n uIntensity: { value: STATE.nebulaIntensity }\n },\n side: THREE.BackSide,\n depthWrite: false\n });\n\n const nebulaSphere = new THREE.Mesh(\n new THREE.SphereGeometry(500, 64, 64),\n nebulaMaterial\n );\n scene.add(nebulaSphere);\n\n // \u2550\u2550\u2550\u2550\u2550\u2550\u2550 POST-PROCESSING COMPOSER \u2550\u2550\u2550\u2550\u2550\u2550\u2550\n const composer = new EffectComposer(renderer);\n composer.addPass(new RenderPass(scene, camera));\n\n const afterimagePass = new AfterimagePass();\n afterimagePass.uniforms['damp'].value = STATE.trails;\n composer.addPass(afterimagePass);\n\n const bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5, 0.4, 0.85);\n bloomPass.threshold = 0.25;\n bloomPass.strength = STATE.bloom;\n bloomPass.radius = 0.45;\n composer.addPass(bloomPass);\n\n const godRaysPass = new ShaderPass(GodRaysShader);\n godRaysPass.enabled = false;\n composer.addPass(godRaysPass);\n\n const lensFlarePass = new ShaderPass(LensFlareShader);\n lensFlarePass.enabled = STATE.lensFlareEnabled && STATE.lensFlareIntensity > 0;\n composer.addPass(lensFlarePass);\n\n const anamorphicPass = new ShaderPass(AnamorphicShader);\n composer.addPass(anamorphicPass);\n\n const chromaticPass = new ShaderPass(ChromaticShader);\n chromaticPass.enabled = STATE.chromaticAberration > 0;\n composer.addPass(chromaticPass);\n\n // Custom DOF pass (radial focus)\n const customDOFPass = new ShaderPass(CustomDOFShader);\n customDOFPass.uniforms.uFocus.value = STATE.dofFocus;\n customDOFPass.uniforms.uFocalLength.value = STATE.dofFocalLength;\n customDOFPass.uniforms.uBokehStrength.value = STATE.dofBokehStrength;\n customDOFPass.uniforms.uResolution.value.set(window.innerWidth, window.innerHeight);\n customDOFPass.enabled = STATE.dofEnabled;\n composer.addPass(customDOFPass);\n\n const vignettePass = new ShaderPass(VignetteShader);\n composer.addPass(vignettePass);\n\n const grainPass = new ShaderPass(GrainShader);\n grainPass.enabled = STATE.filmGrain > 0;\n composer.addPass(grainPass);\n\n const AlphaFromLumaShader = {\n uniforms: { tDiffuse: { value: null } },\n vertexShader: `varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`,\n fragmentShader: `uniform sampler2D tDiffuse; varying vec2 vUv;\n void main() {\n vec3 rgb = texture2D(tDiffuse, vUv).rgb;\n float luminance = max(max(rgb.r, rgb.g), rgb.b);\n float alpha = smoothstep(0.004, 0.06, luminance);\n gl_FragColor = vec4(rgb, alpha);\n }`\n };\n composer.addPass(new OutputPass());\n composer.addPass(new ShaderPass(AlphaFromLumaShader));\n\n // Keep the embedded CSS accents aligned with the kit palette.\n function applyCssTheme(accent) {\n const luminance = accent.r * 0.299 + accent.g * 0.587 + accent.b * 0.114;\n const boost = luminance < 0.35 ? 0.35 / Math.max(luminance, 0.001) : 1;\n for (const [channel, value] of Object.entries({ ar: accent.r, ag: accent.g, ab: accent.b })) {\n document.documentElement.style.setProperty(`--${channel}`, Math.min(Math.round(value * boost * 255), 255));\n }\n }\n applyCssTheme(new THREE.Color(def.c1));\n\n // \u2550\u2550\u2550\u2550\u2550\u2550\u2550 UI & EVENTS \u2550\u2550\u2550\u2550\u2550\u2550\u2550\n const overlay = document.getElementById('overlay');\n const audioControls = document.getElementById('audio-controls');\n const playPauseBtn = document.getElementById('play-pause-btn');\n const progressBar = document.getElementById('progress-bar');\n const progressContainer = document.getElementById('progress-container');\n const timeDisplay = document.getElementById('time-display');\n const trackName = document.getElementById('track-name');\n const status = document.getElementById('status-bar');\n\n const formatTime = s => `${Math.floor(s / 60)}:${Math.floor(s % 60).toString().padStart(2, '0')}`;\n\n function updateUI() {\n if (AUDIO.mode === 'mic') {\n playPauseBtn.textContent = '\u{1F399} live';\n playPauseBtn.disabled = true;\n } else {\n playPauseBtn.textContent = AUDIO.isPlaying ? '\u23F8 pause' : '\u25B6 play';\n playPauseBtn.classList.toggle('playing', AUDIO.isPlaying);\n playPauseBtn.disabled = false;\n }\n if (AUDIO.mode === 'file' && AUDIO.audioBuffer) {\n const cur = AUDIO.getCurrentTime(), dur = AUDIO.getDuration();\n progressBar.style.width = (cur / dur * 100) + '%';\n timeDisplay.textContent = `${formatTime(cur)} / ${formatTime(dur)}`;\n } else {\n progressBar.style.width = '0%';\n timeDisplay.textContent = AUDIO.mode === 'mic' ? 'LIVE' : '0:00 / 0:00';\n }\n }\n\n function togglePlayPause() {\n if (AUDIO.mode !== 'file') return;\n AUDIO.togglePlayPause();\n updateUI();\n status.innerHTML = AUDIO.isPlaying ? 'SYSTEM: <span>PLAYING</span>' : 'SYSTEM: <span>PAUSED</span>';\n }\n\n function activate() {\n AUDIO.init();\n if (AUDIO.ctx.state === 'suspended') AUDIO.ctx.resume();\n overlay.style.opacity = 0;\n setTimeout(() => overlay.style.display = 'none', 800);\n audioControls.classList.add('visible');\n }\n\n async function loadTrack(file) {\n activate();\n const buffer = await AUDIO.loadTrack(await file.arrayBuffer(), file.name);\n const samples = buffer.getChannelData(0);\n const bars = 160;\n const peaks = Array.from({ length: bars }, (_, bar) => {\n const from = Math.floor(bar * samples.length / bars);\n const to = Math.floor((bar + 1) * samples.length / bars);\n const stride = Math.max(1, Math.floor((to - from) / 64));\n let peak = 0;\n for (let i = from; i < to; i += stride) peak = Math.max(peak, Math.abs(samples[i]));\n return peak;\n });\n parent.postMessage({ type: 'ad-qf-waveform', value: peaks }, '*');\n AUDIO.play();\n trackName.textContent = file.name.length > 25 ? file.name.slice(0, 22) + '...' : file.name;\n updateUI();\n status.innerHTML = 'SYSTEM: <span>PLAYING</span>';\n }\n\n async function startMic(preferredLabel = null) {\n console.log('Global startMic requested:', preferredLabel);\n try {\n activate();\n await AUDIO.startMic(preferredLabel);\n trackName.textContent = AUDIO.currentTrackName;\n updateUI();\n // status update handled in AUDIO.startMic\n } catch (e) {\n console.error(\"Global startMic failed:\", e);\n const status = document.getElementById('status-bar');\n status.innerHTML = `ERROR: <span style=\"color:#ff6b6b\">${e.message}</span>`;\n }\n }\n\n // Using optional chaining/checks because one input was replaced by a button\n const enterBtn = document.getElementById('enter-btn');\n if (enterBtn) enterBtn.onclick = () => startMic(null);\n\n const fileInput = document.getElementById('file-input');\n if (fileInput) fileInput.onchange = e => e.target.files[0] && loadTrack(e.target.files[0]);\n\n document.getElementById('file-input-2').onchange = e => { if (e.target.files[0]) loadTrack(e.target.files[0]); e.target.value = ''; };\n document.getElementById('mic-btn-2').onclick = () => startMic(null);\n playPauseBtn.onclick = togglePlayPause;\n progressContainer.onclick = e => {\n if (AUDIO.mode !== 'file' || !AUDIO.audioBuffer) return;\n const rect = progressContainer.getBoundingClientRect();\n AUDIO.seek((e.clientX - rect.left) / rect.width * AUDIO.getDuration());\n };\n document.getElementById('screenshot-btn')?.addEventListener('click', () => {\n composer.render();\n const a = document.createElement('a');\n a.download = 'quantum-' + Date.now() + '.png';\n a.href = renderer.domElement.toDataURL('image/png');\n a.click();\n });\n document.getElementById('fullscreen-btn')?.addEventListener('click', () => {\n document.fullscreenElement ? document.exitFullscreen() : document.documentElement.requestFullscreen();\n });\n\n // \u2550\u2550\u2550\u2550\u2550\u2550\u2550 ANIMATION LOOP \u2550\u2550\u2550\u2550\u2550\u2550\u2550\n let renderVisible = true;\n let connectionElapsed = 1 / 20;\n let idleConnectionElapsed = 2;\n let frameRequest = 0;\n let nextFrameAt = 0;\n let lastRenderedAt = -1;\n let elapsed = 0;\n\n function startFrames() {\n if (!frameRequest && renderVisible && !document.hidden) frameRequest = requestAnimationFrame(animate);\n }\n\n function stopFrames() {\n if (frameRequest) cancelAnimationFrame(frameRequest);\n frameRequest = 0;\n nextFrameAt = 0;\n lastRenderedAt = -1;\n }\n\n document.addEventListener('visibilitychange', () => {\n if (document.hidden) stopFrames();\n else startFrames();\n });\n\n function animate(now) {\n frameRequest = 0;\n if (!renderVisible || document.hidden) return;\n startFrames();\n const frameInterval = 1000 / Math.max(1, STATE.targetFPS);\n if (now < nextFrameAt) return;\n nextFrameAt = Math.max(nextFrameAt + frameInterval, now);\n const dt = lastRenderedAt < 0 ? 0 : Math.min((now - lastRenderedAt) / 1000, 0.05);\n lastRenderedAt = now;\n elapsed += dt;\n\n material.uniforms.uTime.value += dt * STATE.timeScale;\n grainPass.uniforms.uTime.value = elapsed;\n lensFlarePass.uniforms.uTime.value = elapsed;\n\n material.uniforms.uZoomFactor.value = camera.position.length() / CONFIG.defaultZoom;\n material.uniforms.uMousePos.value.lerp(mouseWorldPos, 0.1);\n material.uniforms.uMouseVelocity.value.lerp(mouseVelocity, 0.08);\n\n AUDIO.update();\n\n const s = STATE.sensitivity, lf = 0.16;\n material.uniforms.uSubBass.value = THREE.MathUtils.lerp(material.uniforms.uSubBass.value, AUDIO.gatedBands.subBass * s, lf);\n material.uniforms.uBass.value = THREE.MathUtils.lerp(material.uniforms.uBass.value, AUDIO.gatedBands.bass * s, lf);\n material.uniforms.uLowMid.value = THREE.MathUtils.lerp(material.uniforms.uLowMid.value, AUDIO.gatedBands.lowMid * s, lf);\n material.uniforms.uMid.value = THREE.MathUtils.lerp(material.uniforms.uMid.value, AUDIO.gatedBands.mid * s, lf);\n material.uniforms.uHighMid.value = THREE.MathUtils.lerp(material.uniforms.uHighMid.value, AUDIO.gatedBands.highMid * s, lf);\n material.uniforms.uHigh.value = THREE.MathUtils.lerp(material.uniforms.uHigh.value, AUDIO.gatedBands.high * s, lf);\n material.uniforms.uUltraHigh.value = THREE.MathUtils.lerp(material.uniforms.uUltraHigh.value, AUDIO.gatedBands.ultraHigh * s, lf);\n material.uniforms.uBeatEnergy.value = THREE.MathUtils.lerp(material.uniforms.uBeatEnergy.value, AUDIO.beatEnergy, 0.22);\n material.uniforms.uSpectralCentroid.value = THREE.MathUtils.lerp(material.uniforms.uSpectralCentroid.value, AUDIO.spectralCentroid, lf);\n material.uniforms.uSpectralFlux.value = THREE.MathUtils.lerp(material.uniforms.uSpectralFlux.value, AUDIO.spectralFlux * s, lf);\n\n // === ENHANCEMENT: Update new uniforms ===\n material.uniforms.uOnsetEnergy.value = THREE.MathUtils.lerp(material.uniforms.uOnsetEnergy.value, AUDIO.onsetEnergy, 0.25);\n material.uniforms.uTerrainMode.value = FIELD_TYPES[STATE.field]?.terrain ? 1.0 : 0.0;\n material.uniforms.uTerrainHeight.value = STATE.waveformTerrainHeight;\n\n // Update secondary particles\n secondaryPoints.visible = STATE.secondaryEnabled;\n secondaryMaterial.uniforms.uTime.value = material.uniforms.uTime.value;\n secondaryMaterial.uniforms.uSubBass.value = material.uniforms.uSubBass.value;\n secondaryMaterial.uniforms.uBass.value = material.uniforms.uBass.value;\n secondaryMaterial.uniforms.uLowMid.value = material.uniforms.uLowMid.value;\n secondaryMaterial.uniforms.uMid.value = material.uniforms.uMid.value;\n secondaryMaterial.uniforms.uHighMid.value = material.uniforms.uHighMid.value;\n secondaryMaterial.uniforms.uHigh.value = material.uniforms.uHigh.value;\n secondaryMaterial.uniforms.uUltraHigh.value = material.uniforms.uUltraHigh.value;\n secondaryMaterial.uniforms.uBeatEnergy.value = material.uniforms.uBeatEnergy.value;\n secondaryMaterial.uniforms.uSpectralCentroid.value = material.uniforms.uSpectralCentroid.value;\n secondaryMaterial.uniforms.uSpectralFlux.value = material.uniforms.uSpectralFlux.value;\n secondaryMaterial.uniforms.uOnsetEnergy.value = material.uniforms.uOnsetEnergy.value;\n secondaryMaterial.uniforms.uVortexStrength.value = STATE.vortexStrength;\n\n // Update nebula background\n nebulaSphere.visible = STATE.nebulaEnabled;\n if (STATE.nebulaEnabled) {\n nebulaMaterial.uniforms.uTime.value = material.uniforms.uTime.value;\n nebulaMaterial.uniforms.uBass.value = material.uniforms.uBass.value;\n nebulaMaterial.uniforms.uMid.value = material.uniforms.uMid.value;\n nebulaMaterial.uniforms.uHigh.value = material.uniforms.uHigh.value;\n nebulaMaterial.uniforms.uBeatEnergy.value = material.uniforms.uBeatEnergy.value;\n }\n\n // Update particle connections\n connectionMesh.visible = STATE.connectionsEnabled;\n if (STATE.connectionsEnabled) {\n const activeAudio = AUDIO.mode === 'mic' || AUDIO.isPlaying || AUDIO.externalActive;\n const connectionState = [\n STATE.field, STATE.connectionThreshold, geometry.drawRange.count,\n material.uniforms.uViewportAspect.value, material.uniforms.uViewportHalfHeight.value, STATE.fieldSize,\n STATE.qualityLevel, activeAudio\n ];\n connectionElapsed = Math.min(1 / 20, connectionElapsed + dt);\n idleConnectionElapsed = Math.min(2, idleConnectionElapsed + dt);\n if (connectionElapsed >= 1 / 20 && shouldRebuildConnections(activeAudio, connectionState, lastConnectionState, idleConnectionElapsed)) {\n const refreshSelection = !lastConnectionState || connectionState[2] !== lastConnectionState[2]\n || connectionState[6] !== lastConnectionState[6] || (!activeAudio && idleConnectionElapsed >= 2);\n updateConnections(\n STATE.connectionThreshold,\n activeAudio ? material.uniforms.uBass.value : 0,\n activeAudio ? material.uniforms.uHighMid.value : 0,\n activeAudio ? material.uniforms.uMid.value : 0,\n refreshSelection\n );\n lastConnectionState = connectionState;\n connectionElapsed = 0;\n idleConnectionElapsed = 0;\n }\n connectionMaterial.uniforms.uBeatEnergy.value = material.uniforms.uBeatEnergy.value;\n connectionMaterial.uniforms.uHighMid.value = material.uniforms.uHighMid.value;\n connectionMaterial.uniforms.uSpectralCentroid.value = material.uniforms.uSpectralCentroid.value;\n connectionMaterial.uniforms.uOpacity.value = STATE.connectionOpacity;\n } else {\n connectionElapsed = 1 / 20;\n idleConnectionElapsed = 2;\n lastConnectionState = null;\n }\n\n // Update crawlers\n crawlerMesh.visible = STATE.crawlersEnabled && STATE.connectionsEnabled && (AUDIO.externalActive || AUDIO.active && (AUDIO.isPlaying || AUDIO.mode === 'mic'));\n if (crawlerMesh.visible) {\n updateCrawlers(\n dt,\n material.uniforms.uHighMid.value,\n material.uniforms.uBeatEnergy.value,\n material.uniforms.uSpectralCentroid.value\n );\n crawlerTarget.lerpColors(crawlerLow, crawlerHigh, material.uniforms.uSpectralCentroid.value);\n crawlerMaterial.uniforms.uColor.value.lerp(crawlerTarget, 0.05);\n crawlerMaterial.uniforms.uBeatEnergy.value = material.uniforms.uBeatEnergy.value;\n }\n\n // Update post-processing\n chromaticPass.uniforms.uBeatEnergy.value = material.uniforms.uBeatEnergy.value;\n godRaysPass.uniforms.uBeatEnergy.value = material.uniforms.uBeatEnergy.value;\n lensFlarePass.uniforms.uBeatEnergy.value = material.uniforms.uBeatEnergy.value;\n vignettePass.uniforms.uBeatEnergy.value = material.uniforms.uBeatEnergy.value;\n customDOFPass.uniforms.uBeatEnergy.value = material.uniforms.uBeatEnergy.value;\n\n\n\n // Update trails\n trailSystem.update(elapsed, {\n bass: material.uniforms.uBass.value,\n mid: material.uniforms.uMid.value,\n high: material.uniforms.uHigh.value,\n beat: material.uniforms.uBeatEnergy.value\n }, material.uniforms.uColor1.value);\n\n // === ENHANCEMENT: Adaptive Quality System ===\n QUALITY_SYSTEM.update(dt * 1000);\n\n // === ENHANCEMENT: Audio-Reactive Camera ===\n AUDIO_CAMERA.update(dt * 1000);\n\n // Camera shake\n if (STATE.cameraShake > 0 && AUDIO.beatEnergy > 0.1) {\n const shake = AUDIO.beatEnergy * STATE.cameraShake;\n cameraShakeOffset.set((Math.random() - 0.5) * shake * 0.6, (Math.random() - 0.5) * shake * 0.4, (Math.random() - 0.5) * shake * 0.25);\n camera.position.copy(baseCameraPos).add(cameraShakeOffset);\n }\n\n controls.autoRotateSpeed = 0.35 + material.uniforms.uMid.value * 2.0 + material.uniforms.uBeatEnergy.value * 2.0;\n bloomPass.strength = STATE.bloom + (AUDIO.gatedBands.bass + AUDIO.gatedBands.mid + AUDIO.beatEnergy) / 6;\n bloomPass.enabled = bloomPass.strength > 0;\n anamorphicPass.uniforms.uIntensity.value = STATE.anamorphicStretch + AUDIO.gatedBands.bass * 0.4;\n anamorphicPass.enabled = anamorphicPass.uniforms.uIntensity.value > 0;\n\n controls.update();\n composer.render();\n }\n\n window.addEventListener('resize', () => {\n camera.aspect = window.innerWidth / window.innerHeight;\n camera.updateProjectionMatrix();\n material.uniforms.uViewportAspect.value = window.innerWidth / window.innerHeight;\n secondaryMaterial.uniforms.uViewportAspect.value = material.uniforms.uViewportAspect.value;\n material.uniforms.uViewportHalfHeight.value = Math.tan(camera.fov * Math.PI / 360) * CONFIG.defaultZoom;\n secondaryMaterial.uniforms.uViewportHalfHeight.value = material.uniforms.uViewportHalfHeight.value;\n renderer.setSize(window.innerWidth, window.innerHeight);\n composer.setSize(window.innerWidth, window.innerHeight);\n STATE.pixelRatio = fitPixelRatio();\n QUALITY_SYSTEM.applyQuality();\n customDOFPass.uniforms.uResolution.value.set(window.innerWidth, window.innerHeight);\n baseCameraPos.copy(camera.position);\n });\n\n let pausedFromKit = false;\n let requestedTimeScale = STATE.timeScale;\n const sideEffects = {\n trails: value => afterimagePass.uniforms['damp'].value = value,\n chromaticAberration: value => { chromaticPass.uniforms.uIntensity.value = value; chromaticPass.enabled = value > 0; },\n filmGrain: value => { grainPass.uniforms.uIntensity.value = value; grainPass.enabled = value > 0; },\n nebulaIntensity: value => nebulaMaterial.uniforms.uIntensity.value = value,\n lensFlareEnabled: value => lensFlarePass.enabled = value && STATE.lensFlareIntensity > 0,\n lensFlareIntensity: value => { lensFlarePass.uniforms.uIntensity.value = value; lensFlarePass.enabled = STATE.lensFlareEnabled && value > 0; },\n dofEnabled: value => customDOFPass.enabled = value,\n dofFocus: value => customDOFPass.uniforms.uFocus.value = value,\n dofFocalLength: value => customDOFPass.uniforms.uFocalLength.value = value,\n dofBokehStrength: value => customDOFPass.uniforms.uBokehStrength.value = value,\n godRaysEnabled: value => godRaysPass.enabled = value && STATE.godRaysIntensity > 0,\n godRaysIntensity: value => { godRaysPass.uniforms.uIntensity.value = value; godRaysPass.enabled = STATE.godRaysEnabled && value > 0; },\n vortexStrength: value => material.uniforms.uVortexStrength.value = value,\n pulseIntensity: value => material.uniforms.uPulseIntensity.value = value,\n fieldSize: value => { material.uniforms.uFieldFill.value = value; secondaryMaterial.uniforms.uFieldFill.value = value; },\n };\n window.addEventListener('message', ({ source, data }) => {\n if (source !== parent || data?.type !== 'ad-qf-control') return;\n const { key, value } = data;\n if (key === 'field' && FIELD_TYPES[value]) {\n STATE.field = value;\n const selected = FIELD_TYPES[value];\n material.uniforms.uNoiseScale.value = selected.s;\n material.uniforms.uCurlStrength.value = selected.curl;\n material.uniforms.uSizeBase.value = selected.sz;\n } else if (key === 'visible') {\n renderVisible = !!value;\n if (renderVisible) startFrames();\n else stopFrames();\n }\n else if (key === 'interactive') controls.enabled = !!value;\n else if (key === 'spectrum') EXTERNAL.set(value?.bands, value?.range);\n else if (key === 'particleCount') {\n particleCount = Number.isFinite(value) ? Math.max(0, Math.floor(value)) : null;\n QUALITY_SYSTEM.applyQuality();\n }\n else if (key === 'particleBudget') {\n particleBudget = Number.isFinite(value) ? Math.max(0, Math.floor(value)) : Infinity;\n QUALITY_SYSTEM.applyQuality();\n }\n else if (key === 'timeScale') { requestedTimeScale = value; if (!pausedFromKit) STATE.timeScale = value; }\n else if (key === 'paused') { pausedFromKit = !!value; STATE.timeScale = pausedFromKit ? 0 : requestedTimeScale; }\n else if (key === 'play') togglePlayPause();\n else if (key === 'file' && value) loadTrack(value);\n else if (key === 'mic') {\n if (AUDIO.mode === 'mic') { AUDIO.stopCurrentSource(); AUDIO.mode = null; }\n else startMic(null);\n }\n else if (key === 'seek') { AUDIO.seek(value); updateUI(); }\n else if (key === 'screenshot') document.getElementById('screenshot-btn').click();\n else if (key === 'palette' && Array.isArray(value)) {\n const main = new THREE.Color(value[0]), light = new THREE.Color(value[1]);\n applyCssTheme(main);\n material.uniforms.uColor1.value.copy(main);\n material.uniforms.uColor2.value.copy(light);\n material.uniforms.uColor3.value.copy(light).lerp(new THREE.Color('#ffffff'), 0.32);\n secondaryMaterial.uniforms.uColor1.value.copy(main);\n secondaryMaterial.uniforms.uColor2.value.copy(light);\n nebulaMaterial.uniforms.uColor1.value.copy(main);\n nebulaMaterial.uniforms.uColor2.value.copy(light);\n nebulaMaterial.uniforms.uColor3.value.copy(main);\n connectionMaterial.uniforms.uColor.value.copy(light);\n connectionMaterial.uniforms.uColor2.value.copy(main);\n const white = new THREE.Color('#ffffff');\n const tint = glowColor(light.clone().lerp(white, 0.35), 0.75);\n lensFlarePass.uniforms.uTint.value.copy(tint);\n anamorphicPass.uniforms.uTint.value.copy(tint);\n // Reaction lines stand out from the field like the presets' contrasting colours did:\n // a luminous tint of the theme's light colour with a near-white core on bright passages.\n glowColor(crawlerLow.copy(light).lerp(white, 0.2), 0.75);\n glowColor(crawlerHigh.copy(light).lerp(white, 0.5), 0.95);\n crawlerMaterial.uniforms.uColor.value.copy(crawlerLow);\n trailSystem.trails.forEach((trail, i) => glowColor(trail.color.copy(main).lerp(light, i / Math.max(1, trailSystem.trails.length - 1)).lerp(white, 0.15), 0.6));\n } else if (Object.prototype.hasOwnProperty.call(STATE, key)) {\n STATE[key] = value;\n if (key === 'density') QUALITY_SYSTEM.applyQuality();\n sideEffects[key]?.(value);\n }\n });\n let lastTransport = '';\n setInterval(() => {\n if (!renderVisible || document.hidden) return;\n const value = {\n mode: AUDIO.mode, playing: AUDIO.isPlaying,\n current: AUDIO.mode === 'file' ? AUDIO.getCurrentTime() : 0,\n duration: AUDIO.mode === 'file' ? AUDIO.getDuration() : 0,\n track: AUDIO.mode ? AUDIO.currentTrackName : ''\n };\n const snapshot = JSON.stringify(value);\n if (snapshot !== lastTransport) {\n lastTransport = snapshot;\n parent.postMessage({ type: 'ad-qf-transport', value }, '*');\n }\n }, 250);\n parent.postMessage({ type: 'ad-qf-ready' }, '*');\n\n QUALITY_SYSTEM.applyQuality();\n startFrames();\n <\/script>\n</body>\n\n</html>\n";
5079
+ var reference_default = "<!DOCTYPE html>\n<!-- Quantum Fields Contributors, MIT License. Full notice: UPSTREAM-LICENSE.txt in this package. -->\n<html lang=\"en\">\n\n<head>\n <meta charset=\"UTF-8\">\n <title>Quantum Field: Interactive</title>\n <style>\n /* The kit hides this page's own interface, so it loads no web fonts: the scene makes no requests beyond three.js. */\n\n * {\n box-sizing: border-box;\n }\n\n :root {\n --ar: 167; --ag: 139; --ab: 250;\n }\n\n html, body {\n margin: 0;\n overflow: hidden;\n background: transparent !important;\n color-scheme: dark;\n font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;\n }\n\n canvas {\n display: block;\n }\n\n #scanlines {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n background: radial-gradient(ellipse at center, transparent 0%, transparent 50%, rgba(0, 0, 0, 0.5) 100%);\n pointer-events: none;\n z-index: 10;\n }\n\n #overlay {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n justify-content: center;\n align-items: center;\n background: linear-gradient(180deg, rgba(15, 10, 25, 0.97) 0%, rgba(10, 10, 20, 0.98) 100%);\n z-index: 100;\n backdrop-filter: blur(20px);\n transition: opacity 1s ease;\n pointer-events: none;\n /* Allow clicks to pass through to canvas */\n }\n\n .hud-panel {\n background: linear-gradient(165deg, rgba(255, 255, 255, 0.04) 0%, rgba(255, 255, 255, 0.01) 100%);\n border: 1px solid rgba(255, 255, 255, 0.07);\n padding: 55px 75px;\n text-align: center;\n border-radius: 28px;\n box-shadow: 0 30px 80px rgba(0, 0, 0, 0.5);\n pointer-events: auto;\n /* Re-enable clicks for buttons */\n }\n\n h1 {\n font-family: 'Space Grotesk', sans-serif;\n font-weight: 500;\n letter-spacing: 1px;\n font-size: 34px;\n margin: 0 0 8px 0;\n background: linear-gradient(135deg, #ffffff 0%, rgba(var(--ar),var(--ag),var(--ab),0.75) 40%, rgb(var(--ar),var(--ag),var(--ab)) 100%);\n -webkit-background-clip: text;\n -webkit-text-fill-color: transparent;\n background-clip: text;\n }\n\n .hud-panel p {\n color: rgba(255, 255, 255, 0.35);\n font-size: 13px;\n margin-bottom: 40px;\n letter-spacing: 0.5px;\n font-weight: 300;\n }\n\n .btn-group {\n display: flex;\n gap: 14px;\n justify-content: center;\n }\n\n button,\n .file-upload {\n background: linear-gradient(135deg, rgba(var(--ar),var(--ag),var(--ab),0.12) 0%, rgba(var(--ar),var(--ag),var(--ab),0.06) 100%);\n border: 1px solid rgba(var(--ar),var(--ag),var(--ab),0.25);\n color: rgba(255, 255, 255, 0.85);\n padding: 15px 30px;\n font-size: 13px;\n font-weight: 500;\n cursor: pointer;\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\n letter-spacing: 0.3px;\n border-radius: 14px;\n font-family: 'Inter', sans-serif;\n }\n\n button:hover,\n .file-upload:hover {\n background: linear-gradient(135deg, rgba(var(--ar),var(--ag),var(--ab),0.25) 0%, rgba(var(--ar),var(--ag),var(--ab),0.18) 100%);\n border-color: rgba(var(--ar),var(--ag),var(--ab),0.45);\n transform: translateY(-2px);\n box-shadow: 0 12px 35px rgba(139, 92, 246, 0.2);\n }\n\n input[type=\"file\"] {\n display: none;\n }\n\n #status-bar {\n position: absolute;\n bottom: 80px;\n left: 28px;\n color: rgba(255, 255, 255, 0.3);\n font-size: 10px;\n letter-spacing: 0.5px;\n z-index: 200;\n pointer-events: none;\n }\n\n #status-bar span {\n color: rgba(196, 181, 253, 0.7);\n font-weight: 500;\n }\n\n #interaction-hint {\n position: absolute;\n top: 28px;\n width: 100%;\n text-align: center;\n color: rgba(255, 255, 255, 0.5);\n font-size: 11px;\n letter-spacing: 2px;\n pointer-events: none;\n z-index: 9;\n text-transform: lowercase;\n }\n\n #zoom-indicator {\n position: absolute;\n bottom: 80px;\n right: 28px;\n color: rgba(255, 255, 255, 0.3);\n font-size: 10px;\n z-index: 20;\n pointer-events: none;\n }\n\n #zoom-indicator span {\n color: rgba(196, 181, 253, 0.7);\n }\n\n #audio-controls {\n position: fixed;\n bottom: 24px;\n left: 50%;\n transform: translateX(-50%);\n display: flex;\n gap: 10px;\n align-items: center;\n padding: 12px 20px;\n background: linear-gradient(180deg, rgba(35, 28, 50, 0.88) 0%, rgba(22, 20, 38, 0.92) 100%);\n border: 1px solid rgba(255, 255, 255, 0.05);\n border-radius: 18px;\n z-index: 200;\n opacity: 0;\n pointer-events: none;\n transition: all 0.4s ease;\n backdrop-filter: blur(25px);\n }\n\n #audio-controls.visible {\n opacity: 1;\n pointer-events: auto;\n }\n\n #audio-controls button,\n #audio-controls .file-upload {\n padding: 9px 14px;\n font-size: 11px;\n background: rgba(255, 255, 255, 0.03);\n border: 1px solid rgba(255, 255, 255, 0.07);\n color: rgba(255, 255, 255, 0.65);\n border-radius: 10px;\n }\n\n #play-pause-btn {\n min-width: 78px;\n 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;\n border-color: rgba(var(--ar),var(--ag),var(--ab),0.28) !important;\n color: #e9d5ff !important;\n }\n\n #play-pause-btn.playing {\n background: linear-gradient(135deg, rgba(251, 113, 133, 0.18) 0%, rgba(244, 114, 182, 0.12) 100%) !important;\n border-color: rgba(251, 113, 133, 0.35) !important;\n }\n\n #progress-container {\n width: 85px;\n height: 4px;\n background: rgba(255, 255, 255, 0.06);\n border-radius: 4px;\n cursor: pointer;\n }\n\n #progress-bar {\n height: 100%;\n 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));\n border-radius: 4px;\n width: 0%;\n }\n\n #time-display {\n color: rgba(255, 255, 255, 0.35);\n font-size: 10px;\n min-width: 62px;\n text-align: center;\n }\n\n #track-name {\n color: rgba(255, 255, 255, 0.3);\n font-size: 10px;\n max-width: 85px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n /* --- Center Panel Refinement --- */\n .hud-panel {\n background: rgba(15, 10, 25, 0.85);\n border: 1px solid rgba(255, 255, 255, 0.08);\n /* Darkened container */\n padding: 24px;\n /* Reduced padding */\n border-radius: 24px;\n box-shadow: 0 20px 50px rgba(0, 0, 0, 0.6);\n backdrop-filter: blur(40px);\n min-width: 420px;\n /* Wider for buttons */\n }\n\n .hud-panel h1,\n .hud-panel p {\n display: none;\n /* Hide title as per screenshot showing only buttons */\n }\n\n .btn-group {\n display: flex;\n gap: 16px;\n justify-content: center;\n width: 100%;\n }\n\n .btn-group button {\n flex: 1;\n background: transparent;\n border: 1px solid rgba(var(--ar),var(--ag),var(--ab),0.3);\n color: rgb(var(--ar),var(--ag),var(--ab));\n padding: 14px 0;\n border-radius: 12px;\n font-size: 12px;\n letter-spacing: 1px;\n text-transform: uppercase;\n font-weight: 600;\n transition: all 0.3s ease;\n }\n\n .btn-group button:hover {\n background: rgba(var(--ar),var(--ag),var(--ab),0.1);\n border-color: rgb(var(--ar),var(--ag),var(--ab));\n box-shadow: 0 0 20px rgba(var(--ar),var(--ag),var(--ab),0.2);\n }\n #interaction-hint, #audio-controls, #status-bar, #zoom-indicator { display: none !important; }\n </style>\n</head>\n\n<body>\n <div id=\"scanlines\" style=\"display:none\"></div>\n <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>\n <div id=\"overlay\" style=\"display:none\">\n <div class=\"hud-panel\">\n <h1>Quantum Fields</h1>\n <p>Audio-reactive particle simulation</p>\n <div class=\"btn-group\">\n <button id=\"enter-btn\">Enter</button>\n </div>\n </div>\n </div>\n <div id=\"audio-controls\">\n <button id=\"play-pause-btn\" class=\"playing\">\u23F8 pause</button>\n <div id=\"progress-container\">\n <div id=\"progress-bar\"></div>\n </div>\n <span id=\"time-display\">0:00 / 0:00</span>\n <span id=\"track-name\">no track</span>\n <label class=\"file-upload\" title=\"Load audio\">\u{1F4C1}<input type=\"file\" id=\"file-input-2\" accept=\"audio/*\"></label>\n <button id=\"mic-btn-2\" title=\"Microphone\">\u{1F399}</button>\n <button id=\"screenshot-btn\" title=\"Screenshot\">\u{1F4F7}</button>\n <button id=\"fullscreen-btn\" title=\"Fullscreen\">\u26F6</button>\n </div>\n <div id=\"status-bar\"><span>ready</span> \xB7 awaiting input</div>\n <div id=\"zoom-indicator\">zoom <span id=\"zoom-level\">100%</span></div>\n\n <!-- Global Error Handler (Must be before module imports) -->\n <script>\n window.onerror = function (msg, url, lineNo, columnNo, error) {\n const status = document.getElementById('status-bar');\n if (status) {\n status.innerHTML = `ERROR: <span style=\"color:#ff6b6b\">${msg}</span>`;\n status.style.opacity = 1;\n }\n console.error('Global Error:', msg, url, lineNo, error);\n return false;\n };\n // Catch unhandled promise rejections\n window.onunhandledrejection = function (event) {\n const status = document.getElementById('status-bar');\n if (status) {\n status.innerHTML = `ERROR: <span style=\"color:#ff6b6b\">${event.reason}</span>`;\n status.style.opacity = 1;\n }\n console.error('Unhandled Rejection:', event.reason);\n };\n <\/script>\n\n <script type=\"importmap\">\n{\n \"imports\": {\n \"three\": \"https://unpkg.com/three@0.182.0/build/three.module.js\",\n \"three/addons/\": \"https://unpkg.com/three@0.182.0/examples/jsm/\"\n }\n}\n<\/script>\n\n <script type=\"module\">\n import * as THREE from 'three';\n import { OrbitControls } from 'three/addons/controls/OrbitControls.js';\n import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';\n import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';\n import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';\n import { AfterimagePass } from 'three/addons/postprocessing/AfterimagePass.js';\n import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';\n // BokehPass removed - using custom DOF shader\n import { OutputPass } from 'three/addons/postprocessing/OutputPass.js';\n\n function fitPixelRatio() {\n return Math.min(window.devicePixelRatio || 1, 1.5,\n Math.sqrt(2_100_000 / Math.max(1, window.innerWidth * window.innerHeight)));\n }\n\n const CONFIG = {\n maxParticles: 250000,\n maxDensity: 0.14,\n secondaryParticles: 50000,\n initialDensity: 0.043,\n fieldRadius: 90,\n minZoom: 1,\n maxZoom: 250,\n defaultZoom: 200\n };\n let particleBudget = Number(window.frameElement?.getAttribute('data-particle-budget') ?? Infinity);\n particleBudget = Number.isFinite(particleBudget) ? Math.max(0, Math.floor(particleBudget)) : Infinity;\n // Requested number of particles across both layers; null follows the density setting.\n let particleCount = window.frameElement?.getAttribute('data-particle-count');\n particleCount = particleCount != null && Number.isFinite(Number(particleCount)) ? Math.max(0, Math.floor(Number(particleCount))) : null;\n\n const STATE = {\n field: 'Gluon (Strong)',\n density: CONFIG.initialDensity,\n fieldSize: 0.78,\n sensitivity: 1.1,\n trails: 0.66144,\n bloom: 0.3,\n timeScale: 1,\n pixelRatio: fitPixelRatio(),\n chromaticAberration: 0.004,\n filmGrain: 0.001,\n cameraShake: 0.65,\n vortexStrength: 0.73,\n pulseIntensity: 0.15,\n anamorphicStretch: 0.1,\n bassGateThreshold: 0.12,\n bassGateAttack: 0.3,\n bassGateRelease: 0.136,\n bassGateRatio: 2.142,\n bassGateEnabled: false,\n // NEW EFFECTS\n godRaysEnabled: false,\n godRaysIntensity: 0.078,\n godRaysDecay: 0.92,\n dofEnabled: false,\n dofFocus: 0.04,\n dofFocalLength: 0.40,\n dofBokehStrength: 0.36,\n lensFlareEnabled: true,\n lensFlareIntensity: 0.1,\n particleTrailsEnabled: true,\n particleTrailOpacity: 1,\n nebulaEnabled: false,\n nebulaIntensity: 0.3,\n connectionsEnabled: true,\n connectionThreshold: 20.0,\n connectionOpacity: 0.75,\n // === ENHANCEMENT PACK v2.0 ===\n // 1. Motion Blur\n motionBlurEnabled: true,\n motionBlurStrength: 0.1,\n // 2. Adaptive Quality\n adaptiveQualityEnabled: true,\n targetFPS: 45,\n qualityLevel: 1,\n // 3. Onset Detection\n onsetSensitivity: 1,\n onsetDecay: 0.92,\n // 4. Audio-Reactive Camera\n audioCameraEnabled: true,\n audioCameraSmoothing: 0.2,\n audioCameraIntensity: 0.3,\n // 5. Waveform Terrain\n waveformTerrainScale: 1.0,\n waveformTerrainHeight: 20.0\n };\n\n // --- AUDIO SYSTEM ---\n // Spectrum supplied by the host app (e.g. a native audio engine the iframe cannot hear):\n // band levels 0..1, log-spaced from `low` to `high` Hz. It is turned into the same frequency\n // array an analyser would give, so bands, beats, flux, onsets and the terrain all follow it.\n const EXTERNAL = {\n target: null, low: 40, high: 12000,\n sampleRate: 48000, data: new Uint8Array(2048), smoothed: new Float32Array(2048), prev: new Float32Array(2048),\n fresh() { return this.target !== null; },\n set(bands, range) {\n this.target = Array.isArray(bands) && bands.length ? bands.map((v) => Math.max(0, Math.min(1, Number(v) || 0))) : null;\n if (Array.isArray(range) && range[0] > 0 && range[1] > range[0]) [this.low, this.high] = range;\n },\n // Host frames arrive at their own pace (often 20 Hz); easing toward them every rendered\n // frame keeps the flux continuous, like an analyser's smoothing does.\n fill() {\n const bands = this.target, n = bands.length, len = this.data.length, nyquist = this.sampleRate / 2;\n const span = Math.log(this.high / this.low);\n for (let i = 0; i < len; i++) {\n const freq = Math.max(1, (i + 0.5) / len * nyquist);\n const pos = Math.log(freq / this.low) / span * (n - 1);\n let level;\n if (pos <= 0) level = bands[0];\n else if (pos >= n - 1) level = bands[n - 1] * Math.max(0, 1 - Math.log(freq / this.high) / Math.log(nyquist / this.high));\n else { const k = Math.floor(pos), t = pos - k; level = bands[k] * (1 - t) + bands[k + 1] * t; }\n this.smoothed[i] += (level - this.smoothed[i]) * 0.35;\n this.data[i] = Math.min(255, Math.round(this.smoothed[i] * 255));\n }\n }\n };\n\n const AUDIO = {\n ctx: null, analyser: null, source: null, gainNode: null, data: null,\n audioBuffer: null, isPlaying: false, isPaused: false,\n startTime: 0, pauseTime: 0, currentTrackName: 'No Track',\n mode: null, micStream: null,\n bands: { subBass: 0, bass: 0, lowMid: 0, mid: 0, highMid: 0, high: 0, ultraHigh: 0 },\n gatedBands: { subBass: 0, bass: 0, lowMid: 0, mid: 0, highMid: 0, high: 0, ultraHigh: 0 },\n beatThreshold: 0.15, beatDecay: 0.93, beatEnergy: 0,\n lastBeatTime: 0, beatDetected: false, peakHistory: [],\n spectralCentroid: 0, spectralFlux: 0, prevSpectrum: null, active: false, idleCleared: false,\n // === ENHANCEMENT: Onset Detection ===\n onsetEnergy: 0, onsetDetected: false, onsetHistory: [], onsetThreshold: 0,\n // === ENHANCEMENT: Waveform Data ===\n waveformData: new Float32Array(128), waveformSmoothed: new Float32Array(128),\n gateState: {\n isOpen: false, envelope: 0, holdTime: 0, noiseFloor: 0.05,\n calibrationSamples: [], isCalibrating: true, calibrationFrames: 30\n },\n\n init() {\n if (this.ctx) return;\n this.ctx = new (window.AudioContext || window.webkitAudioContext)();\n this.analyser = this.ctx.createAnalyser();\n this.analyser.fftSize = 4096;\n this.analyser.smoothingTimeConstant = 0.65;\n this.data = new Uint8Array(this.analyser.frequencyBinCount);\n this.prevSpectrum = new Float32Array(this.analyser.frequencyBinCount);\n this.active = true;\n },\n\n loadTrack(arrayBuffer, fileName) {\n return new Promise((resolve, reject) => {\n this.ctx.decodeAudioData(arrayBuffer, (buffer) => {\n this.stopCurrentSource();\n this.audioBuffer = buffer;\n this.currentTrackName = fileName || 'Unknown';\n this.mode = 'file';\n this.pauseTime = 0;\n this.resetCalibration();\n resolve(buffer);\n }, reject);\n });\n },\n\n play() {\n if (!this.audioBuffer || this.mode !== 'file' || this.isPlaying) return;\n this.source = this.ctx.createBufferSource();\n this.source.buffer = this.audioBuffer;\n this.source.loop = true;\n this.source.connect(this.analyser);\n this.analyser.connect(this.ctx.destination);\n this.source.start(0, this.pauseTime);\n this.startTime = this.ctx.currentTime - this.pauseTime;\n this.isPlaying = true;\n this.isPaused = false;\n },\n\n pause() {\n if (!this.isPlaying || this.mode !== 'file') return;\n this.pauseTime = this.getCurrentTime();\n this.isPaused = true;\n this.isPlaying = false;\n if (this.source) { try { this.source.stop(); } catch (e) { } this.source.disconnect(); this.source = null; }\n },\n\n togglePlayPause() {\n if (this.mode === 'mic') return false;\n if (this.isPlaying) { this.pause(); return false; }\n else { this.play(); return true; }\n },\n\n getCurrentTime() {\n if (!this.isPlaying) return this.pauseTime;\n if (!this.audioBuffer) return 0;\n return (this.ctx.currentTime - this.startTime) % this.audioBuffer.duration;\n },\n\n getDuration() { return this.audioBuffer ? this.audioBuffer.duration : 0; },\n\n seek(time) {\n if (this.mode !== 'file' || !this.audioBuffer) return;\n const wasPlaying = this.isPlaying;\n if (wasPlaying) this.pause();\n this.pauseTime = Math.max(0, Math.min(time, this.audioBuffer.duration));\n if (wasPlaying) this.play();\n },\n\n stopCurrentSource() {\n if (this.source) { try { this.source.stop(); this.source.disconnect(); } catch (e) { } this.source = null; }\n if (this.gainNode) { try { this.gainNode.disconnect(); } catch (e) { } this.gainNode = null; }\n if (this.micStream) { this.micStream.getTracks().forEach(t => t.stop()); this.micStream = null; }\n this.isPlaying = false; this.isPaused = false; this.pauseTime = 0;\n },\n\n async startMic(preferredLabel = null) {\n console.log('startMic called with preferredLabel:', preferredLabel);\n const status = document.getElementById('status-bar');\n if (status) status.innerHTML = 'SYSTEM: <span>REQUESTING ACCESS...</span>';\n\n try {\n // Ensure context is running\n if (this.ctx.state === 'suspended') {\n console.log('Resuming AudioContext...');\n await this.ctx.resume();\n }\n\n // 1. Get initial stream (activates permissions so labels are visible)\n console.log('Requesting initial getUserMedia...');\n let stream = await navigator.mediaDevices.getUserMedia({ audio: true });\n console.log('Initial stream obtained:', stream.id);\n\n // 2. Search for preferred device (loopback)\n let target = null;\n if (preferredLabel) {\n const devices = await navigator.mediaDevices.enumerateDevices();\n console.log(\"Audio Devices Found:\", devices.length, devices.map(d => `${d.label} (${d.kind})`));\n\n target = devices.find(d => d.kind === 'audioinput' &&\n d.label.toLowerCase().includes(preferredLabel.toLowerCase()));\n }\n\n if (target) {\n console.log(`Switching audio input to: ${target.label} [${target.deviceId}]`);\n stream.getTracks().forEach(t => t.stop()); // Stop generic stream\n\n // Request specific device - being explicit with constraints\n stream = await navigator.mediaDevices.getUserMedia({\n audio: {\n deviceId: { exact: target.deviceId },\n echoCancellation: false,\n noiseSuppression: false,\n autoGainControl: false,\n channelCount: 2\n }\n });\n this.currentTrackName = target.label;\n } else {\n if (preferredLabel) console.warn(`Preferred device '${preferredLabel}' not found. Using default.`);\n // Keep the initial stream\n this.currentTrackName = stream.getAudioTracks()[0]?.label || 'Live Microphone';\n }\n\n this.stopCurrentSource();\n this.micStream = stream;\n this.source = this.ctx.createMediaStreamSource(stream);\n this.gainNode = this.ctx.createGain();\n this.gainNode.gain.value = 3.0; // Boost loopback/mic signal before FFT \u2014 tune via sensitivity slider\n this.source.connect(this.gainNode);\n this.gainNode.connect(this.analyser);\n this.mode = 'mic';\n this.isPlaying = true;\n this.resetCalibration();\n\n if (status) status.innerHTML = 'SYSTEM: <span>LISTENING (' + (preferredLabel ? 'Loopback' : 'Mic') + ')</span>';\n updateUI();\n\n return stream;\n } catch (e) {\n console.error(\"AUDIO.startMic failed:\", e);\n throw e;\n }\n },\n\n resetCalibration() { this.gateState.isCalibrating = true; this.gateState.calibrationSamples = []; },\n\n applyBassGate(rawValue) {\n if (!STATE.bassGateEnabled) return rawValue;\n const { bassGateThreshold: threshold, bassGateAttack: attack, bassGateRelease: release, bassGateRatio: ratio } = STATE;\n if (this.gateState.isCalibrating) {\n this.gateState.calibrationSamples.push(rawValue);\n if (this.gateState.calibrationSamples.length >= this.gateState.calibrationFrames) {\n const sorted = [...this.gateState.calibrationSamples].sort((a, b) => a - b);\n this.gateState.noiseFloor = sorted.slice(0, Math.floor(sorted.length * 0.2)).reduce((a, b) => a + b, 0) / Math.floor(sorted.length * 0.2);\n this.gateState.isCalibrating = false;\n }\n }\n const effectiveThreshold = Math.max(threshold, this.gateState.noiseFloor * 1.5);\n if (rawValue > effectiveThreshold) { this.gateState.isOpen = true; this.gateState.holdTime = 10; }\n else if (rawValue < effectiveThreshold * 0.7 && this.gateState.holdTime <= 0) { this.gateState.isOpen = false; }\n if (this.gateState.holdTime > 0) this.gateState.holdTime--;\n this.gateState.envelope += ((this.gateState.isOpen ? 1.0 : 0.0) - this.gateState.envelope) * (this.gateState.isOpen ? attack : release);\n let output = rawValue;\n if (this.gateState.isOpen && rawValue > effectiveThreshold) {\n output = effectiveThreshold + (rawValue - effectiveThreshold) / ratio;\n }\n return output * this.gateState.envelope;\n },\n\n update() {\n const own = this.active && this.isPlaying && this.source;\n this.externalActive = !own && EXTERNAL.fresh();\n if (!own && !this.externalActive) {\n if (!this.idleCleared) {\n for (const band in this.bands) this.bands[band] = this.gatedBands[band] = 0;\n this.spectralCentroid = this.spectralFlux = 0;\n this.beatDetected = this.onsetDetected = false;\n this.idleCleared = true;\n }\n this.beatEnergy *= this.beatDecay;\n this.onsetEnergy *= STATE.onsetDecay;\n return;\n }\n this.idleCleared = false;\n let data = this.data, prevSpectrum = this.prevSpectrum, nyquist;\n if (this.externalActive) {\n EXTERNAL.fill();\n data = EXTERNAL.data; prevSpectrum = EXTERNAL.prev; nyquist = EXTERNAL.sampleRate / 2;\n } else {\n this.analyser.getByteFrequencyData(this.data);\n nyquist = this.ctx.sampleRate / 2;\n }\n const len = data.length;\n const getFreqIndex = (freq) => Math.round(freq / nyquist * len);\n const ranges = { subBass: [50, 120], bass: [120, 200], lowMid: [200, 450], mid: [450, 2000], highMid: [2000, 3500], high: [3500, 12000], ultraHigh: [12000, 20000] };\n let totalEnergy = 0, weightedFreqSum = 0;\n for (const [band, [low, high]] of Object.entries(ranges)) {\n const lowIdx = getFreqIndex(low), highIdx = getFreqIndex(high);\n let sum = 0;\n for (let i = lowIdx; i < highIdx && i < len; i++) {\n const v = data[i] / 255.0;\n sum += v; totalEnergy += v; weightedFreqSum += v * i;\n }\n const rawBand = sum / (highIdx - lowIdx);\n this.bands[band] = rawBand;\n this.gatedBands[band] = (band === 'subBass' || band === 'bass' || band === 'lowMid') ? this.applyBassGate(rawBand) : rawBand;\n }\n this.spectralCentroid = totalEnergy > 0 ? weightedFreqSum / totalEnergy / len : 0;\n let flux = 0;\n for (let i = 0; i < len; i++) {\n const current = data[i] / 255.0, diff = current - prevSpectrum[i];\n flux += diff > 0 ? diff * diff : 0;\n prevSpectrum[i] = current;\n }\n this.spectralFlux = Math.sqrt(flux / len);\n const currentEnergy = this.gatedBands.bass * 0.6 + this.gatedBands.subBass * 0.4;\n this.peakHistory.push(currentEnergy);\n if (this.peakHistory.length > 50) this.peakHistory.shift();\n const avgEnergy = this.peakHistory.reduce((a, b) => a + b, 0) / this.peakHistory.length;\n const variance = this.peakHistory.reduce((a, b) => a + Math.pow(b - avgEnergy, 2), 0) / this.peakHistory.length;\n const dynamicThreshold = avgEnergy + Math.sqrt(variance) * 1.8;\n const now = performance.now();\n if (currentEnergy > dynamicThreshold && currentEnergy > this.beatThreshold && now - this.lastBeatTime > 120) {\n this.beatDetected = true; this.beatEnergy = 1.0; this.lastBeatTime = now;\n } else { this.beatDetected = false; }\n this.beatEnergy *= this.beatDecay;\n\n // === ENHANCEMENT: Improved Onset Detection ===\n // Uses spectral flux derivative for sharper transient response\n const onsetSens = STATE.onsetSensitivity;\n this.onsetHistory.push(this.spectralFlux);\n if (this.onsetHistory.length > 8) this.onsetHistory.shift();\n const recentAvg = this.onsetHistory.slice(-4).reduce((a, b) => a + b, 0) / 4;\n const olderAvg = this.onsetHistory.slice(0, 4).reduce((a, b) => a + b, 0) / Math.max(1, this.onsetHistory.slice(0, 4).length);\n const onsetDelta = (recentAvg - olderAvg) * onsetSens;\n this.onsetDetected = onsetDelta > 0.015 && now - this.lastBeatTime > 50;\n this.onsetEnergy = this.onsetDetected ? 1.0 : this.onsetEnergy * STATE.onsetDecay;\n\n // === ENHANCEMENT: Waveform Data for Terrain Field ===\n const waveformBins = 128;\n const binSize = Math.floor(len / waveformBins);\n for (let i = 0; i < waveformBins; i++) {\n let sum = 0;\n for (let j = 0; j < binSize; j++) {\n sum += data[i * binSize + j] / 255.0;\n }\n const target = sum / binSize;\n this.waveformSmoothed[i] += (target - this.waveformSmoothed[i]) * 0.3;\n this.waveformData[i] = this.waveformSmoothed[i];\n }\n }\n };\n\n // --- SCENE SETUP ---\n const scene = new THREE.Scene();\n scene.fog = new THREE.FogExp2(0x000508, 0.005);\n const camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 0.1, 800);\n camera.position.set(0, 12, CONFIG.defaultZoom);\n const cameraShakeOffset = new THREE.Vector3();\n const baseCameraPos = camera.position.clone();\n const zoomIndicator = document.getElementById('zoom-level');\n\n const renderer = new THREE.WebGLRenderer({ powerPreference: \"high-performance\", antialias: true, alpha: true });\n renderer.setClearColor(0x000000, 0);\n renderer.setSize(window.innerWidth, window.innerHeight);\n renderer.setPixelRatio(STATE.pixelRatio);\n renderer.toneMapping = THREE.ACESFilmicToneMapping;\n renderer.toneMappingExposure = 2.1;\n document.body.appendChild(renderer.domElement);\n\n const controls = new OrbitControls(camera, renderer.domElement);\n controls.enableDamping = true;\n controls.dampingFactor = 0.06;\n controls.autoRotate = false;\n controls.autoRotateSpeed = 0.3;\n controls.minDistance = CONFIG.minZoom;\n controls.maxDistance = CONFIG.maxZoom;\n controls.maxPolarAngle = Math.PI * 0.85;\n controls.minPolarAngle = Math.PI * 0.15;\n\n controls.addEventListener('change', () => {\n const dist = camera.position.length();\n const zoomPercent = Math.round((1 - (dist - CONFIG.minZoom) / (CONFIG.maxZoom - CONFIG.minZoom)) * 100);\n zoomIndicator.textContent = Math.max(0, Math.min(100, zoomPercent)) + '%';\n baseCameraPos.copy(camera.position);\n });\n\n const raycaster = new THREE.Raycaster();\n const mouse = new THREE.Vector2(9999, 9999);\n const plane = new THREE.Plane(new THREE.Vector3(0, 0, 1), 0);\n const mouseWorldPos = new THREE.Vector3();\n let mouseVelocity = new THREE.Vector2();\n let prevMouse = new THREE.Vector2();\n\n window.addEventListener('mousemove', (e) => {\n const newMouse = new THREE.Vector2((e.clientX / window.innerWidth) * 2 - 1, -(e.clientY / window.innerHeight) * 2 + 1);\n mouseVelocity.subVectors(newMouse, prevMouse);\n prevMouse.copy(newMouse);\n mouse.copy(newMouse);\n raycaster.setFromCamera(mouse, camera);\n const target = new THREE.Vector3();\n raycaster.ray.intersectPlane(plane, target);\n if (target) mouseWorldPos.copy(target);\n });\n\n window.addEventListener('keydown', (e) => {\n if (e.key === '+' || e.key === '=') { camera.position.multiplyScalar(0.9); controls.update(); }\n else if (e.key === '-' || e.key === '_') { camera.position.multiplyScalar(1.1); controls.update(); }\n else if (e.key === '0') { camera.position.normalize().multiplyScalar(CONFIG.defaultZoom); controls.update(); }\n else if (e.key === ' ') { e.preventDefault(); togglePlayPause(); }\n else if (e.key === 'f' || e.key === 'F') { STATE.dofEnabled = !STATE.dofEnabled; customDOFPass.enabled = STATE.dofEnabled; }\n });\n\n // Every field type shares one screen-fitted footprint; switching presets never reallocates particles.\n // uFieldFill: share of the viewport the field spans; above 1.0 it runs past the edges.\n const shapeFunction = `\nuniform float uFieldFill;\nvec3 shapedPosition(vec3 seed) {\n vec2 plane = seed.xy * 2.0 - 1.0;\n return vec3(plane.x * uViewportAspect * uViewportHalfHeight * uFieldFill,\n plane.y * uViewportHalfHeight * uFieldFill,\n (seed.z - 0.5) * 12.0);\n}`;\n\n // --- PARTICLE VERTEX SHADER ---\n const vertexShader = `\nuniform float uTime, uPixelRatio, uSizeBase, uNoiseScale, uCurlStrength;\nuniform float uViewportAspect, uViewportHalfHeight;\nuniform vec3 uColor1, uColor2, uColor3;\nuniform float uSubBass, uBass, uLowMid, uMid, uHighMid, uHigh, uUltraHigh;\nuniform float uBeatEnergy, uSpectralCentroid, uSpectralFlux;\nuniform float uOnsetEnergy, uTerrainMode, uTerrainHeight;\nuniform float uVortexStrength, uPulseIntensity, uZoomFactor;\nuniform vec3 uMousePos;\nuniform vec2 uMouseVelocity;\nattribute vec3 aRandom;\nattribute float aPhase, aLayer;\nvarying vec3 vColor;\nvarying float vAlpha, vEnergy, vDepth, vRimLight, vGlint;\n\nvec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }\nvec4 mod289(vec4 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }\nvec4 permute(vec4 x) { return mod289(((x*34.0)+1.0)*x); }\nvec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; }\n\nfloat snoise(vec3 v) {\n const vec2 C = vec2(1.0/6.0, 1.0/3.0);\n const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);\n vec3 i = floor(v + dot(v, C.yyy));\n vec3 x0 = v - i + dot(i, C.xxx);\n vec3 g = step(x0.yzx, x0.xyz);\n vec3 l = 1.0 - g;\n vec3 i1 = min(g.xyz, l.zxy);\n vec3 i2 = max(g.xyz, l.zxy);\n vec3 x1 = x0 - i1 + C.xxx;\n vec3 x2 = x0 - i2 + C.yyy;\n vec3 x3 = x0 - D.yyy;\n i = mod289(i);\n 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));\n float n_ = 0.142857142857;\n vec3 ns = n_ * D.wyz - D.xzx;\n vec4 j = p - 49.0 * floor(p * ns.z * ns.z);\n vec4 x_ = floor(j * ns.z);\n vec4 y_ = floor(j - 7.0 * x_);\n vec4 x = x_ * ns.x + ns.yyyy;\n vec4 y = y_ * ns.x + ns.yyyy;\n vec4 h = 1.0 - abs(x) - abs(y);\n vec4 b0 = vec4(x.xy, y.xy);\n vec4 b1 = vec4(x.zw, y.zw);\n vec4 s0 = floor(b0) * 2.0 + 1.0;\n vec4 s1 = floor(b1) * 2.0 + 1.0;\n vec4 sh = -step(h, vec4(0.0));\n vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy;\n vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww;\n vec3 p0 = vec3(a0.xy, h.x);\n vec3 p1 = vec3(a0.zw, h.y);\n vec3 p2 = vec3(a1.xy, h.z);\n vec3 p3 = vec3(a1.zw, h.w);\n vec4 norm = taylorInvSqrt(vec4(dot(p0, p0), dot(p1, p1), dot(p2, p2), dot(p3, p3)));\n p0 *= norm.x; p1 *= norm.y; p2 *= norm.z; p3 *= norm.w;\n vec4 m = max(0.6 - vec4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), 0.0);\n m = m * m;\n return 42.0 * dot(m * m, vec4(dot(p0, x0), dot(p1, x1), dot(p2, x2), dot(p3, x3)));\n}\n\nvec3 curl(float x, float y, float z) {\n float eps = 0.08;\n return vec3(\n snoise(vec3(x, y+eps, z)) - snoise(vec3(x, y-eps, z)),\n snoise(vec3(x, y, z+eps)) - snoise(vec3(x, y, z-eps)),\n snoise(vec3(x+eps, y, z)) - snoise(vec3(x-eps, y, z))\n );\n}\n\n${shapeFunction}\n\nvoid main() {\n float t = uTime * (1.0 + uMid * 0.5);\n vec3 noisePos = position * 0.035 + aRandom;\n float layerMod = 1.0 + aLayer * 0.5;\n\n vec3 flow1 = curl(noisePos.x * uNoiseScale + t*0.1, noisePos.y * uNoiseScale, noisePos.z * uNoiseScale);\n 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;\n vec3 flow3 = curl(noisePos.x * uNoiseScale * 0.5 - t*0.05, noisePos.y * uNoiseScale * 0.5, noisePos.z * uNoiseScale * 0.5) * 0.6 * uLowMid;\n vec3 flow = flow1 + flow2 * uHigh + flow3;\n\n vec3 newPos = shapedPosition(aRandom) + (flow * uCurlStrength * layerMod * (1.0 + uSpectralFlux * 2.5));\n\n float vortexAngle = uVortexStrength * uBass * 0.45 * (0.75 + aRandom.z * 0.5);\n float cosA = cos(vortexAngle), sinA = sin(vortexAngle);\n vec2 rotated = vec2(newPos.x * cosA - newPos.z * sinA, newPos.x * sinA + newPos.z * cosA);\n newPos.xz = mix(newPos.xz, rotated, uBass * uVortexStrength);\n\n float yVortex = sin(newPos.y * 0.1 + uTime) * uMid * uVortexStrength * 2.0;\n newPos.x += cos(newPos.y * 0.15) * yVortex;\n newPos.z += sin(newPos.y * 0.15) * yVortex;\n\n float distToMouse = distance(newPos, uMousePos);\n float repulsion = smoothstep(25.0, 0.0, distToMouse);\n vec3 repelDir = normalize(newPos - uMousePos + vec3(0.001));\n newPos += repelDir * repulsion * (12.0 + length(uMouseVelocity) * 360.0);\n\n // Audio changes depth throughout the plane, without evacuating its centre.\n float beatWave = sin(aRandom.x * 12.0 + aRandom.y * 7.0 - uTime * 6.0);\n float bassDepth = (beatWave * uBeatEnergy * uPulseIntensity * 3.5\n + sin(uTime * 1.8 + aPhase) * uSubBass * 4.0\n + smoothstep(0.3, 0.8, uBass) * 6.0\n + uOnsetEnergy * 8.0) * (0.6 + aRandom.z * 0.4);\n newPos.z += bassDepth;\n float fieldWave = sin(aRandom.x * 11.0 - uTime * 0.8 + sin(aRandom.y * 7.0 + uTime * 0.35));\n newPos.z += fieldWave * (1.8 + uBass * 2.4 + uBeatEnergy * 1.6);\n\n // Waveform relief moves toward the camera while retaining the shared rectangular footprint.\n if (uTerrainMode > 0.5) {\n float freqBin = aRandom.x;\n float waveHeight = uBass * sin(freqBin * 6.28 * 8.0 + uTime * 3.0) * 0.5;\n waveHeight += uMid * sin(freqBin * 6.28 * 16.0 + uTime * 5.0) * 0.3;\n waveHeight += uHigh * sin(freqBin * 6.28 * 32.0 + uTime * 8.0) * 0.2;\n waveHeight *= (1.0 + uSpectralFlux * 3.0);\n newPos.z += waveHeight * uTerrainHeight;\n newPos.z += sin(aRandom.y * 6.28 * 4.0 + uTime) * uSubBass * 5.0;\n }\n\n vec4 mvPosition = modelViewMatrix * vec4(newPos, 1.0);\n gl_Position = projectionMatrix * mvPosition;\n\n float size = uSizeBase * (0.85 + uBass * 0.5 + uBeatEnergy * 0.5) * (1.0 + (uZoomFactor - 1.0) * 0.3);\n size *= 1.0 + sin(uTime * 10.0 + aPhase * 6.28) * uHigh * 0.35;\n 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);\n size *= 0.85 + vGlint * 2.1;\n gl_PointSize = clamp(max(size * uPixelRatio * (90.0 / -mvPosition.z) * layerMod, vGlint * 4.5 * uPixelRatio), 0.5, 120.0);\n\n float colorMix = smoothstep(0.0, 2.8, length(flow) + uHighMid * 3.0);\n vec3 baseColor = mix(uColor1, uColor2, colorMix);\n baseColor = mix(baseColor, uColor3, uSpectralCentroid * uHigh * 2.5);\n\n float energyLevel = uBeatEnergy * 0.35 + repulsion * uMid * 2.0 + uSpectralFlux * 0.8;\n vColor = mix(baseColor, vec3(1.0), energyLevel * 0.4);\n vColor += vec3(0.08, 0.04, 0.12) * uUltraHigh * 2.5;\n\n vRimLight = pow(1.0 - max(0.0, dot(normalize(-mvPosition.xyz), normalize(newPos))), 3.0);\n vEnergy = energyLevel;\n vDepth = clamp(-mvPosition.z / 120.0, 0.0, 1.0);\n\n float alpha = 1.0 - smoothstep(0.92, 1.05, max(abs(newPos.x) / (uViewportAspect * uViewportHalfHeight * uFieldFill * 1.05), abs(newPos.y) / (uViewportHalfHeight * uFieldFill * 1.05)));\n float sparkle = 1.0 + (aRandom.x > 0.8 ? sin(uTime * 20.0 + aRandom.y * 150.0) * uHigh * 2.5 : 0.0);\n vAlpha = alpha * sparkle * (0.7 + uMid * 0.35) * layerMod;\n}`;\n\n const fragmentShader = `\nvarying vec3 vColor;\nvarying float vAlpha, vEnergy, vDepth, vRimLight, vGlint;\n\nvoid main() {\n vec2 xy = gl_PointCoord.xy - vec2(0.5);\n float r = length(xy);\n if(r > 0.5) discard;\n\n float core = smoothstep(0.16 - vEnergy * 0.04, 0.12 - vEnergy * 0.04, r);\n float glow = exp(-r * r * (20.0 - vEnergy * 10.0));\n float halo = exp(-r * r * 5.0) * 0.35 + exp(-r * r * 2.5) * 0.15;\n 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));\n\n vec3 finalColor = mix(vColor, vec3(1.0), core * 0.58 + glint * 0.38);\n finalColor += vColor * vRimLight * 0.1;\n finalColor = mix(finalColor, vec3(0.02, 0.03, 0.05), vDepth * 0.4);\n\n 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);\n gl_FragColor = vec4(finalColor, finalAlpha);\n}`;\n\n // \u2550\u2550\u2550\u2550\u2550\u2550\u2550 POST-PROCESSING SHADERS \u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n // GOD RAYS SHADER (Radial Light Scattering)\n const GodRaysShader = {\n uniforms: {\n tDiffuse: { value: null },\n uCenter: { value: new THREE.Vector2(0.5, 0.5) },\n uIntensity: { value: 0.35 },\n uDecay: { value: 0.94 },\n uBeatEnergy: { value: 0.0 }\n },\n vertexShader: `varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`,\n fragmentShader: `\n uniform sampler2D tDiffuse;\n uniform vec2 uCenter;\n uniform float uIntensity, uDecay, uBeatEnergy;\n varying vec2 vUv;\n\n void main() {\n vec4 color = texture2D(tDiffuse, vUv);\n vec2 dir = vUv - uCenter;\n float dist = length(dir);\n dir = normalize(dir);\n\n vec3 rays = vec3(0.0);\n float decay = 1.0;\n\n for(int i = 0; i < 32; i++) {\n float t = float(i) / 32.0;\n vec2 samplePos = vUv - dir * t * 0.15 * (1.0 + uBeatEnergy);\n vec4 s = texture2D(tDiffuse, samplePos);\n float lum = dot(s.rgb, vec3(0.3, 0.59, 0.11));\n rays += s.rgb * decay * step(0.35, lum);\n decay *= uDecay;\n }\n rays /= 16.0;\n rays *= (1.0 - smoothstep(0.3, 0.9, dist));\n\n gl_FragColor = vec4(color.rgb + rays * uIntensity * (1.0 + uBeatEnergy * 0.8), color.a);\n }`\n };\n\n // LENS FLARE POST-PROCESS SHADER (Cinematic)\n const LensFlareShader = {\n uniforms: {\n tDiffuse: { value: null },\n uIntensity: { value: 1.0 },\n uBeatEnergy: { value: 0.0 },\n uTime: { value: 0.0 },\n uTint: { value: new THREE.Color(0.6, 0.7, 1.0) }\n },\n vertexShader: `varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`,\n fragmentShader: `\n uniform sampler2D tDiffuse;\n uniform float uIntensity, uBeatEnergy, uTime;\n uniform vec3 uTint;\n varying vec2 vUv;\n\n float sdHex(vec2 p) {\n p = abs(p);\n return max(p.x * 0.866 + p.y * 0.5, p.y) - 1.0;\n }\n\n void main() {\n vec4 color = texture2D(tDiffuse, vUv);\n vec2 center = vec2(0.5);\n vec2 toCenter = center - vUv;\n float dist = length(toCenter);\n vec2 dir = normalize(toCenter + 0.0001);\n\n vec3 flare = vec3(0.0);\n\n // Sample bright areas for flare source\n float brightness = dot(color.rgb, vec3(0.299, 0.587, 0.114));\n float threshold = smoothstep(0.6, 0.9, brightness);\n\n // Anamorphic horizontal streak\n float streak = 0.0;\n for(float i = -8.0; i <= 8.0; i += 1.0) {\n vec2 streakUV = vUv + vec2(i * 0.012, 0.0);\n vec4 streakSample = texture2D(tDiffuse, streakUV);\n float streakBright = dot(streakSample.rgb, vec3(0.299, 0.587, 0.114));\n float falloff = 1.0 - abs(i) / 10.0;\n streak += smoothstep(0.7, 1.0, streakBright) * falloff * falloff;\n }\n flare += uTint * streak * 0.08;\n\n // Subtle ghosts (reflections through lens elements)\n for(int i = 1; i <= 3; i++) {\n float scale = 1.0 - float(i) * 0.25;\n vec2 ghostUV = center + (center - vUv) * scale * 0.8;\n if(ghostUV.x > 0.0 && ghostUV.x < 1.0 && ghostUV.y > 0.0 && ghostUV.y < 1.0) {\n vec4 ghostSample = texture2D(tDiffuse, ghostUV);\n float ghostBright = dot(ghostSample.rgb, vec3(0.299, 0.587, 0.114));\n float ghostAlpha = smoothstep(0.5, 0.8, ghostBright) * (0.25 / float(i));\n vec3 ghostTint = mix(vec3(1.0), uTint, 0.6) * (1.0 - float(i) * 0.2);\n flare += ghostSample.rgb * ghostTint * ghostAlpha;\n }\n }\n\n // Soft circular halo around bright center\n float halo = smoothstep(0.25, 0.15, abs(dist - 0.2)) * threshold * 0.25;\n flare += uTint * 0.85 * halo;\n\n // Subtle vignette-edge glow\n float edgeGlow = smoothstep(0.5, 0.7, dist) * (1.0 - smoothstep(0.7, 0.85, dist));\n flare += uTint * 0.55 * edgeGlow * brightness * 0.15;\n\n gl_FragColor = vec4(color.rgb + flare * uIntensity * (1.0 + uBeatEnergy * 0.3), color.a);\n }`\n };\n\n // ANAMORPHIC FLARE SHADER\n const AnamorphicShader = {\n uniforms: {\n tDiffuse: { value: null },\n uStretch: { value: 0.1 },\n uThreshold: { value: 0.7 },\n uIntensity: { value: 0.4 },\n uTint: { value: new THREE.Color(0.6, 0.8, 1.0) }\n },\n vertexShader: `varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`,\n fragmentShader: `\n uniform sampler2D tDiffuse;\n uniform float uStretch, uThreshold, uIntensity;\n uniform vec3 uTint;\n varying vec2 vUv;\n void main() {\n vec4 color = texture2D(tDiffuse, vUv);\n vec3 streak = vec3(0.0);\n for(float i = -15.0; i <= 15.0; i++) {\n vec4 s = texture2D(tDiffuse, vUv + vec2(i * uStretch * 0.01, 0.0));\n float bright = dot(s.rgb, vec3(0.299, 0.587, 0.114));\n if(bright > uThreshold) {\n float w = 1.0 - abs(i) / 15.0;\n streak += s.rgb * w * w;\n }\n }\n gl_FragColor = vec4(color.rgb + streak / 7.5 * uTint * uIntensity, color.a);\n }`\n };\n\n // CHROMATIC ABERRATION SHADER\n const ChromaticShader = {\n uniforms: {\n tDiffuse: { value: null },\n uIntensity: { value: 0.004 },\n uBeatEnergy: { value: 0.0 }\n },\n vertexShader: `varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`,\n fragmentShader: `\n uniform sampler2D tDiffuse;\n uniform float uIntensity, uBeatEnergy;\n varying vec2 vUv;\n void main() {\n vec2 dir = vUv - vec2(0.5);\n float dist = length(dir);\n vec2 offset = dir * dist * uIntensity * (1.0 + uBeatEnergy * 1.5);\n gl_FragColor = vec4(\n texture2D(tDiffuse, vUv + offset * 1.2).r,\n texture2D(tDiffuse, vUv).g,\n texture2D(tDiffuse, vUv - offset * 1.2).b,\n texture2D(tDiffuse, vUv).a\n );\n }`\n };\n\n // FILM GRAIN SHADER\n const GrainShader = {\n uniforms: {\n tDiffuse: { value: null },\n uTime: { value: 0.0 },\n uIntensity: { value: 0.0 }\n },\n vertexShader: `varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`,\n fragmentShader: `\n uniform sampler2D tDiffuse;\n uniform float uTime, uIntensity;\n varying vec2 vUv;\n float rand(vec2 co) { return fract(sin(dot(co, vec2(12.9898,78.233))) * 43758.5453); }\n void main() {\n vec4 color = texture2D(tDiffuse, vUv);\n float grain = (rand(vUv + fract(uTime)) * 2.0 - 1.0) * uIntensity;\n gl_FragColor = vec4(color.rgb + grain * (1.0 - dot(color.rgb, vec3(0.299, 0.587, 0.114)) * 0.5), color.a);\n }`\n };\n\n // CUSTOM DEPTH OF FIELD SHADER (Radial + Edge blur)\n const CustomDOFShader = {\n uniforms: {\n tDiffuse: { value: null },\n uFocus: { value: 0.05 },\n uFocalLength: { value: 0.25 },\n uBokehStrength: { value: 0.5 },\n uResolution: { value: new THREE.Vector2() },\n uBeatEnergy: { value: 0.0 }\n },\n vertexShader: `\n varying vec2 vUv;\n void main() {\n vUv = uv;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n }\n `,\n fragmentShader: `\n uniform sampler2D tDiffuse;\n uniform float uFocus;\n uniform float uFocalLength;\n uniform float uBokehStrength;\n uniform vec2 uResolution;\n uniform float uBeatEnergy;\n varying vec2 vUv;\n\n void main() {\n vec2 center = vec2(0.5);\n float dist = length(vUv - center);\n\n // Focus ring - sharp in a ring around center, blurry inside and outside\n float focusRing = abs(dist - uFocus);\n float blur = smoothstep(0.0, uFocalLength, focusRing) * uBokehStrength;\n\n vec3 color = vec3(0.0);\n float total = 0.0;\n vec2 texel = 1.0 / uResolution;\n\n if(blur < 0.01) {\n color = texture2D(tDiffuse, vUv).rgb;\n total = 1.0;\n } else {\n // Golden angle spiral sampling\n const float GOLDEN_ANGLE = 2.39996323;\n const int SAMPLES = 24;\n float radius = blur * 20.0 * (1.0 + uBeatEnergy * 0.1);\n\n for(int i = 0; i < SAMPLES; i++) {\n float angle = float(i) * GOLDEN_ANGLE;\n float r = sqrt(float(i) / float(SAMPLES)) * radius;\n vec2 offset = vec2(cos(angle), sin(angle)) * r * texel;\n vec3 s = texture2D(tDiffuse, vUv + offset).rgb;\n\n // Subtle highlight boost\n float lum = dot(s, vec3(0.299, 0.587, 0.114));\n s *= 1.0 + smoothstep(0.7, 1.0, lum) * blur * 0.3;\n\n color += s;\n total += 1.0;\n }\n }\n\n gl_FragColor = vec4(color / total, texture2D(tDiffuse, vUv).a);\n }\n `\n };\n\n // VIGNETTE + GLOW SHADER\n const VignetteShader = {\n uniforms: {\n tDiffuse: { value: null },\n uBeatEnergy: { value: 0.0 }\n },\n vertexShader: `varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`,\n fragmentShader: `\n uniform sampler2D tDiffuse;\n uniform float uBeatEnergy;\n varying vec2 vUv;\n void main() {\n vec4 color = texture2D(tDiffuse, vUv);\n vec2 uv = vUv * (1.0 - vUv.yx);\n float vig = pow(uv.x * uv.y * 15.0, 0.38 * (1.0 - uBeatEnergy * 0.15));\n float lum = dot(color.rgb, vec3(0.299, 0.587, 0.114));\n float glow = smoothstep(0.5, 0.9, lum) * 0.1 * (1.0 + uBeatEnergy * 0.5);\n gl_FragColor = vec4(color.rgb * vig * (1.0 + glow), color.a);\n }`\n };\n\n // \u2550\u2550\u2550\u2550\u2550\u2550\u2550 GEOMETRY \u2550\u2550\u2550\u2550\u2550\u2550\u2550\n const geometry = new THREE.BufferGeometry();\n const mainCapacity = Math.ceil(CONFIG.maxParticles * CONFIG.maxDensity);\n const pos = new Float32Array(mainCapacity * 3);\n const rnd = new Float32Array(mainCapacity * 3);\n const phase = new Float32Array(mainCapacity);\n const layer = new Float32Array(mainCapacity);\n const vec = new THREE.Vector3();\n\n for (let i = 0; i < mainCapacity; i++) {\n vec.setFromSphericalCoords(\n CONFIG.fieldRadius * Math.pow(Math.random(), 0.33),\n Math.acos(2 * Math.random() - 1),\n Math.random() * Math.PI * 2\n );\n pos[i * 3] = vec.x;\n pos[i * 3 + 1] = vec.y;\n pos[i * 3 + 2] = vec.z;\n rnd[i * 3] = Math.random();\n rnd[i * 3 + 1] = Math.random();\n rnd[i * 3 + 2] = Math.random();\n phase[i] = Math.random() * Math.PI * 2;\n }\n\n // Secondary particles now use separate instanced system below\n\n geometry.setAttribute('position', new THREE.BufferAttribute(pos, 3));\n geometry.setAttribute('aRandom', new THREE.BufferAttribute(rnd, 3));\n geometry.setAttribute('aPhase', new THREE.BufferAttribute(phase, 1));\n geometry.setAttribute('aLayer', new THREE.BufferAttribute(layer, 1));\n geometry.setDrawRange(0, Math.floor(CONFIG.maxParticles * STATE.density));\n\n const FIELD_TYPES = {\n 'Photon (EM)': { c1: '#00f2ff', c2: '#0051ff', c3: '#ffffff', s: 1.0, curl: 1.0, sz: 3.0, sc1: '#66d9ff', sc2: '#99eeff' },\n 'Gluon (Strong)': { c1: '#ff0055', c2: '#7000ff', c3: '#ff8800', s: 2.5, curl: 2.4, sz: 3.4, sc1: '#cc66ff', sc2: '#ff6699' },\n 'Higgs (Mass)': { c1: '#ffae00', c2: '#ff4800', c3: '#ffff00', s: 0.6, curl: 0.3, sz: 4.0, sc1: '#ffcc66', sc2: '#ffdd99' },\n 'Gravity': { c1: '#ffffff', c2: '#4466ff', c3: '#8844ff', s: 0.75, curl: 0.18, sz: 2.8, sc1: '#aabbff', sc2: '#ccaaff' },\n 'Dark Energy': { c1: '#1a0033', c2: '#6600ff', c3: '#ff00ff', s: 1.9, curl: 1.6, sz: 3.1, sc1: '#9933ff', sc2: '#cc66ff' },\n 'Neutrino': { c1: '#00ff88', c2: '#00ffcc', c3: '#88ffff', s: 3.0, curl: 3.0, sz: 2.6, sc1: '#66ffbb', sc2: '#99ffdd' },\n // === ENHANCEMENT: Waveform Terrain Field ===\n 'Waveform Terrain': { c1: '#ff3366', c2: '#33ff99', c3: '#6633ff', s: 0.4, curl: 0.15, sz: 2.4, sc1: '#ff6699', sc2: '#66ffcc', terrain: true }\n };\n\n // === ENHANCEMENT: Adaptive Quality System ===\n const QUALITY_SYSTEM = {\n frameCount: 0,\n frameTimes: [],\n lastAdjustTime: 0,\n currentFPS: 60,\n\n update(deltaTime) {\n this.frameCount++;\n this.frameTimes.push(deltaTime);\n if (this.frameTimes.length > 30) this.frameTimes.shift();\n\n if (this.frameCount % 30 === 0) {\n const avgDelta = this.frameTimes.reduce((a, b) => a + b, 0) / this.frameTimes.length;\n this.currentFPS = 1000 / avgDelta;\n\n if (STATE.adaptiveQualityEnabled && performance.now() - this.lastAdjustTime > 1000) {\n if (this.currentFPS < STATE.targetFPS - 10 && STATE.qualityLevel > 0.3) {\n STATE.qualityLevel = Math.max(0.3, STATE.qualityLevel - (this.currentFPS < STATE.targetFPS * 0.6 ? 0.2 : 0.1));\n this.applyQuality();\n this.lastAdjustTime = performance.now();\n } else if (this.currentFPS > STATE.targetFPS + 5 && STATE.qualityLevel < 1.0) {\n STATE.qualityLevel = Math.min(1.0, STATE.qualityLevel + 0.05);\n this.applyQuality();\n this.lastAdjustTime = performance.now();\n }\n }\n }\n },\n\n applyQuality() {\n const q = STATE.qualityLevel;\n const pixelRatio = Math.min(STATE.pixelRatio, Math.max(0.35, STATE.pixelRatio * q));\n const requestedMain = Math.floor(CONFIG.maxParticles * Math.min(STATE.density, CONFIG.maxDensity));\n const densityTotal = requestedMain + CONFIG.secondaryParticles;\n const requestedTotal = particleCount === null ? densityTotal : Math.min(particleCount, mainCapacity + CONFIG.secondaryParticles);\n const visibleParticles = Math.floor(Math.min(requestedTotal, particleBudget) * q);\n // Both layers keep the density's mix; whatever one layer cannot hold goes to the other.\n let mainCount = Math.min(mainCapacity, Math.round(visibleParticles * requestedMain / densityTotal));\n mainCount = Math.min(mainCapacity, Math.max(mainCount, visibleParticles - CONFIG.secondaryParticles));\n geometry.setDrawRange(0, mainCount);\n secondaryGeometry.instanceCount = visibleParticles - mainCount;\n if (renderer.getPixelRatio() !== pixelRatio) {\n renderer.setPixelRatio(pixelRatio);\n composer.setPixelRatio(pixelRatio);\n }\n material.uniforms.uPixelRatio.value = pixelRatio;\n secondaryMaterial.uniforms.uPixelRatio.value = pixelRatio;\n }\n };\n\n // === ENHANCEMENT: Audio-Reactive Camera System ===\n const AUDIO_CAMERA = {\n targetOffset: new THREE.Vector3(),\n currentOffset: new THREE.Vector3(),\n targetFOV: 55,\n baseFOV: 55,\n breathPhase: 0,\n\n update(deltaTime) {\n if (!STATE.audioCameraEnabled || !(AUDIO.active || AUDIO.externalActive)) return;\n\n const bass = AUDIO.gatedBands.bass;\n const mid = AUDIO.gatedBands.mid;\n const high = AUDIO.gatedBands.high;\n const intensity = STATE.audioCameraIntensity;\n const smooth = STATE.audioCameraSmoothing;\n\n // Bass-driven zoom pulse\n this.targetFOV = this.baseFOV - bass * 8 * intensity + AUDIO.beatEnergy * 5 * intensity;\n camera.fov += (this.targetFOV - camera.fov) * smooth;\n camera.updateProjectionMatrix();\n\n // Subtle sway based on mid frequencies\n this.breathPhase += deltaTime * 0.001 * (1 + mid * 2);\n this.targetOffset.x = Math.sin(this.breathPhase * 0.7) * mid * 3 * intensity;\n this.targetOffset.y = Math.cos(this.breathPhase * 0.5) * high * 2 * intensity;\n this.targetOffset.z = Math.sin(this.breathPhase * 0.3) * bass * 4 * intensity;\n\n // Smooth interpolation\n this.currentOffset.lerp(this.targetOffset, smooth);\n\n // Apply onset punch\n if (AUDIO.onsetDetected) {\n this.currentOffset.z -= AUDIO.onsetEnergy * 2 * intensity;\n }\n }\n };\n\n const def = FIELD_TYPES[STATE.field];\n const material = new THREE.ShaderMaterial({\n vertexShader, fragmentShader,\n uniforms: {\n uTime: { value: 0 },\n uPixelRatio: { value: STATE.pixelRatio },\n uSizeBase: { value: def.sz },\n uNoiseScale: { value: def.s },\n uCurlStrength: { value: def.curl },\n uViewportAspect: { value: window.innerWidth / window.innerHeight },\n uViewportHalfHeight: { value: Math.tan(camera.fov * Math.PI / 360) * CONFIG.defaultZoom },\n uFieldFill: { value: STATE.fieldSize },\n uColor1: { value: new THREE.Color(def.c1) },\n uColor2: { value: new THREE.Color(def.c2) },\n uColor3: { value: new THREE.Color(def.c3) },\n uSubBass: { value: 0 }, uBass: { value: 0 }, uLowMid: { value: 0 },\n uMid: { value: 0 }, uHighMid: { value: 0 }, uHigh: { value: 0 }, uUltraHigh: { value: 0 },\n uBeatEnergy: { value: 0 }, uSpectralCentroid: { value: 0 }, uSpectralFlux: { value: 0 },\n uOnsetEnergy: { value: 0 }, uTerrainMode: { value: 0 }, uTerrainHeight: { value: 15.0 },\n uVortexStrength: { value: STATE.vortexStrength },\n uPulseIntensity: { value: STATE.pulseIntensity },\n uZoomFactor: { value: 1.0 },\n uMousePos: { value: new THREE.Vector3(999, 999, 999) },\n uMouseVelocity: { value: new THREE.Vector2(0, 0) }\n },\n transparent: true, depthWrite: false, blending: THREE.AdditiveBlending\n });\n\n const points = new THREE.Points(geometry, material);\n scene.add(points);\n\n // \u2550\u2550\u2550\u2550\u2550\u2550\u2550 INSTANCED SECONDARY PARTICLES \u2550\u2550\u2550\u2550\u2550\u2550\u2550\n const secondaryVertexShader = `\nuniform float uTime, uPixelRatio;\nuniform float uViewportAspect, uViewportHalfHeight;\nuniform float uSubBass, uBass, uLowMid, uMid, uHighMid, uHigh, uUltraHigh;\nuniform float uBeatEnergy, uSpectralCentroid, uSpectralFlux, uOnsetEnergy;\nuniform vec3 uColor1, uColor2;\nuniform float uVortexStrength;\n\nattribute vec3 instancePosition;\nattribute vec3 instanceRandom;\nattribute float instancePhase;\n\nvarying vec3 vColor;\nvarying float vAlpha;\nvarying float vEnergy;\nvarying float vDepth;\n\nvec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }\nvec4 mod289(vec4 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }\nvec4 permute(vec4 x) { return mod289(((x*34.0)+1.0)*x); }\nvec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; }\n\nfloat snoise(vec3 v) {\n const vec2 C = vec2(1.0/6.0, 1.0/3.0);\n const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);\n vec3 i = floor(v + dot(v, C.yyy));\n vec3 x0 = v - i + dot(i, C.xxx);\n vec3 g = step(x0.yzx, x0.xyz);\n vec3 l = 1.0 - g;\n vec3 i1 = min(g.xyz, l.zxy);\n vec3 i2 = max(g.xyz, l.zxy);\n vec3 x1 = x0 - i1 + C.xxx;\n vec3 x2 = x0 - i2 + C.yyy;\n vec3 x3 = x0 - D.yyy;\n i = mod289(i);\n 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));\n float n_ = 0.142857142857;\n vec3 ns = n_ * D.wyz - D.xzx;\n vec4 j = p - 49.0 * floor(p * ns.z * ns.z);\n vec4 x_ = floor(j * ns.z);\n vec4 y_ = floor(j - 7.0 * x_);\n vec4 x = x_ * ns.x + ns.yyyy;\n vec4 y = y_ * ns.x + ns.yyyy;\n vec4 h = 1.0 - abs(x) - abs(y);\n vec4 b0 = vec4(x.xy, y.xy);\n vec4 b1 = vec4(x.zw, y.zw);\n vec4 s0 = floor(b0) * 2.0 + 1.0;\n vec4 s1 = floor(b1) * 2.0 + 1.0;\n vec4 sh = -step(h, vec4(0.0));\n vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy;\n vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww;\n vec3 p0 = vec3(a0.xy, h.x);\n vec3 p1 = vec3(a0.zw, h.y);\n vec3 p2 = vec3(a1.xy, h.z);\n vec3 p3 = vec3(a1.zw, h.w);\n vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2,p2), dot(p3,p3)));\n p0 *= norm.x; p1 *= norm.y; p2 *= norm.z; p3 *= norm.w;\n vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);\n m = m * m;\n return 42.0 * dot(m*m, vec4(dot(p0,x0), dot(p1,x1), dot(p2,x2), dot(p3,x3)));\n}\n\n${shapeFunction}\n\nvoid main() {\n // Secondary particles run at a slower timescale \u2014 they are the atmosphere, not the core\n float t = uTime * 0.45;\n\n // Multi-scale noise flow \u2014 low freqs drive large drift, high freqs add fine turbulence\n vec3 noisePos = instancePosition * 0.035 + instanceRandom;\n vec3 drift = vec3(\n snoise(noisePos + t * 0.06),\n snoise(noisePos * 0.9 - t * 0.05),\n snoise(noisePos * 1.1 + t * 0.04)\n ) * (5.0 + uSubBass * 10.0 + uLowMid * 6.0);\n\n vec3 turbulence = vec3(\n snoise(noisePos * 2.5 + t * 0.18),\n snoise(noisePos * 2.5 - t * 0.14),\n snoise(noisePos * 2.5 + t * 0.12)\n ) * (uHigh * 5.0 + uSpectralFlux * 8.0);\n\n vec3 offset = drift + turbulence;\n\n // Orbital drift \u2014 vortex-aware, modulated by bass\n float orbitSpeed = 0.12 + instanceRandom.x * 0.15 + uBass * 0.08;\n float angle = uTime * orbitSpeed + instancePhase;\n float orbitRadius = length(instancePosition.xz) * (1.0 + uSubBass * 0.25);\n // Vortex coupling \u2014 secondary shell rotates with the main field vortex\n float vortexInfluence = uVortexStrength * uBass * 0.4;\n float vAngle = angle + vortexInfluence;\n vec3 orbit = vec3(\n cos(vAngle) * orbitRadius - instancePosition.x,\n sin(uTime * 0.5 + instancePhase) * (uMid * 4.0 + uLowMid * 2.0),\n sin(vAngle) * orbitRadius - instancePosition.z\n ) * 0.07;\n\n vec3 newPos = shapedPosition(instanceRandom) + offset + orbit;\n\n // Transients and sub-bass move the atmosphere in depth, not away from the centre.\n newPos.z += uOnsetEnergy * 14.0 * (0.6 + instanceRandom.z * 0.4);\n newPos.z += sin(uTime * 1.2 + instancePhase) * uSubBass * 5.0;\n\n vec4 mvPosition = modelViewMatrix * vec4(newPos, 1.0);\n gl_Position = projectionMatrix * mvPosition;\n\n // Size: driven by different bands than the main system\n // Sparkle on ultraHigh, swell on subBass, punch on beatEnergy\n float sparkle = 1.0 + (instanceRandom.x > 0.85 ? sin(uTime * 15.0 + instanceRandom.y * 200.0) * uUltraHigh * 3.0 : 0.0);\n float swell = 1.0 + uSubBass * 0.6 + uLowMid * 0.3;\n float size = (1.1 + uBeatEnergy * 0.7) * swell * sparkle;\n gl_PointSize = clamp(size * uPixelRatio * (55.0 / -mvPosition.z), 0.5, 55.0);\n\n // Colour: spectral centroid shifts hue, highMid brightens toward white\n float centroidMix = clamp(uSpectralCentroid * 2.2 + uHighMid * 0.4, 0.0, 1.0);\n vColor = mix(uColor1, uColor2, centroidMix);\n // SubBass warmth \u2014 hint of deep colour saturation on heavy lows\n vColor = mix(vColor, vColor * 1.3, uSubBass * 0.4);\n // Beat flash \u2014 brief white-hot core\n vColor = mix(vColor, vec3(1.0), uBeatEnergy * 0.25);\n\n // Alpha: outer shell fades at edge, mid-frequency presence sustains visibility\n float edgeFade = 1.0 - smoothstep(0.92, 1.05, max(abs(newPos.x) / (uViewportAspect * uViewportHalfHeight * uFieldFill * 1.05), abs(newPos.y) / (uViewportHalfHeight * uFieldFill * 1.05)));\n vAlpha = edgeFade * (0.45 + uMid * 0.35 + uLowMid * 0.2) * sparkle;\n\n vEnergy = uBeatEnergy * 0.4 + uHighMid * 0.3 + uOnsetEnergy * 0.3;\n vDepth = clamp(-mvPosition.z / 160.0, 0.0, 1.0);\n}\n`;\n\n const secondaryFragmentShader = `\nvarying vec3 vColor;\nvarying float vAlpha;\nvarying float vEnergy;\nvarying float vDepth;\n\nvoid main() {\n vec2 xy = gl_PointCoord.xy - vec2(0.5);\n float r = length(xy);\n if (r > 0.5) discard;\n\n // Wider, softer glow than main particles \u2014 atmospheric haze\n float glow = exp(-r * r * 8.0);\n float softGlow = exp(-r * r * 3.0) * 0.4;\n float core = smoothstep(0.18 - vEnergy * 0.06, 0.10, r);\n\n vec3 finalColor = mix(vColor, vec3(1.0, 0.97, 0.92), core * 0.55);\n // Depth haze \u2014 far particles shift slightly cooler/dimmer\n finalColor = mix(finalColor, vec3(0.02, 0.03, 0.06), vDepth * 0.35);\n\n float finalAlpha = clamp(vAlpha * (glow * 0.65 + softGlow + core * 0.35), 0.0, 1.0);\n gl_FragColor = vec4(finalColor, finalAlpha);\n}\n`;\n\n // Create instanced geometry for secondary particles\n const secondaryGeometry = new THREE.InstancedBufferGeometry();\n\n // Base geometry (single point)\n const basePositions = new Float32Array([0, 0, 0]);\n secondaryGeometry.setAttribute('position', new THREE.BufferAttribute(basePositions, 3));\n\n // Instance attributes\n const instancePositions = new Float32Array(CONFIG.secondaryParticles * 3);\n const instanceRandoms = new Float32Array(CONFIG.secondaryParticles * 3);\n const instancePhases = new Float32Array(CONFIG.secondaryParticles);\n\n for (let i = 0; i < CONFIG.secondaryParticles; i++) {\n // Distribute in outer shell\n const r = CONFIG.fieldRadius * (0.65 + Math.random() * 0.45);\n const theta = Math.acos(2 * Math.random() - 1);\n const phi = Math.random() * Math.PI * 2;\n\n instancePositions[i * 3] = r * Math.sin(theta) * Math.cos(phi);\n instancePositions[i * 3 + 1] = r * Math.sin(theta) * Math.sin(phi);\n instancePositions[i * 3 + 2] = r * Math.cos(theta);\n\n instanceRandoms[i * 3] = Math.random();\n instanceRandoms[i * 3 + 1] = Math.random();\n instanceRandoms[i * 3 + 2] = Math.random();\n\n instancePhases[i] = Math.random() * Math.PI * 2;\n }\n\n secondaryGeometry.setAttribute('instancePosition', new THREE.InstancedBufferAttribute(instancePositions, 3));\n secondaryGeometry.setAttribute('instanceRandom', new THREE.InstancedBufferAttribute(instanceRandoms, 3));\n secondaryGeometry.setAttribute('instancePhase', new THREE.InstancedBufferAttribute(instancePhases, 1));\n\n const secondaryMaterial = new THREE.ShaderMaterial({\n vertexShader: secondaryVertexShader,\n fragmentShader: secondaryFragmentShader,\n uniforms: {\n uTime: { value: 0 },\n uPixelRatio: { value: STATE.pixelRatio },\n uSubBass: { value: 0 },\n uBass: { value: 0 },\n uLowMid: { value: 0 },\n uMid: { value: 0 },\n uHighMid: { value: 0 },\n uHigh: { value: 0 },\n uUltraHigh: { value: 0 },\n uBeatEnergy: { value: 0 },\n uSpectralCentroid: { value: 0 },\n uSpectralFlux: { value: 0 },\n uOnsetEnergy: { value: 0 },\n uVortexStrength: { value: STATE.vortexStrength },\n uColor1: { value: new THREE.Color(def.sc1) },\n uColor2: { value: new THREE.Color(def.sc2) },\n uViewportAspect: { value: window.innerWidth / window.innerHeight },\n uViewportHalfHeight: { value: Math.tan(camera.fov * Math.PI / 360) * CONFIG.defaultZoom },\n uFieldFill: { value: STATE.fieldSize }\n },\n transparent: true,\n depthWrite: false,\n blending: THREE.AdditiveBlending\n });\n\n const secondaryPoints = new THREE.Points(secondaryGeometry, secondaryMaterial);\n scene.add(secondaryPoints);\n\n // Expose secondary-layer controls to the host component.\n STATE.secondaryEnabled = true;\n STATE.secondaryOpacity = 1.0;\n\n // \u2550\u2550\u2550\u2550\u2550\u2550\u2550 PARTICLE TRAIL SYSTEM \u2550\u2550\u2550\u2550\u2550\u2550\u2550\n const particleSeeds = geometry.getAttribute('aRandom');\n function fieldParticlePosition(index, target) {\n const halfHeight = material.uniforms.uViewportHalfHeight.value * STATE.fieldSize;\n const halfWidth = halfHeight * material.uniforms.uViewportAspect.value;\n return target.set(\n (particleSeeds.getX(index) * 2 - 1) * halfWidth,\n (particleSeeds.getY(index) * 2 - 1) * halfHeight,\n (particleSeeds.getZ(index) - 0.5) * 12\n );\n }\n\n class TrailSystem {\n constructor(count, length) {\n this.trails = [];\n this.meshes = [];\n this.nextPosition = new THREE.Vector3();\n\n for (let i = 0; i < count; i++) {\n const anchor = new THREE.Vector2(Math.random() * 1.5 - 0.75, Math.random() * 1.5 - 0.75);\n const start = new THREE.Vector3(\n anchor.x * material.uniforms.uViewportAspect.value * material.uniforms.uViewportHalfHeight.value * STATE.fieldSize,\n anchor.y * material.uniforms.uViewportHalfHeight.value * STATE.fieldSize,\n 0\n );\n const trail = {\n positions: Array.from({ length }, () => start.clone()),\n anchor,\n phase: Math.random() * Math.PI * 2,\n speed: 0.3 + Math.random() * 0.7,\n color: new THREE.Color(def.c1).lerp(new THREE.Color(def.c2), i / Math.max(1, count - 1))\n };\n this.trails.push(trail);\n\n const geom = new THREE.BufferGeometry();\n const positions = new Float32Array(length * 3);\n const alphas = new Float32Array(length);\n for (let j = 0; j < length; j++) alphas[j] = 1.0 - j / length;\n\n geom.setAttribute('position', new THREE.BufferAttribute(positions, 3));\n geom.setAttribute('alpha', new THREE.BufferAttribute(alphas, 1));\n\n const mat = new THREE.ShaderMaterial({\n vertexShader: `attribute float alpha; varying float vAlpha; void main() { vAlpha = alpha; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`,\n 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)); }`,\n uniforms: { uColor: { value: trail.color }, uOpacity: { value: 0.4 }, uBeat: { value: 0 } },\n transparent: true, depthWrite: false, blending: THREE.AdditiveBlending\n });\n\n const line = new THREE.Line(geom, mat);\n this.meshes.push(line);\n scene.add(line);\n }\n }\n\n update(time, audio, fieldColor) {\n const halfHeight = material.uniforms.uViewportHalfHeight.value * STATE.fieldSize;\n const halfWidth = halfHeight * material.uniforms.uViewportAspect.value;\n for (let i = 0; i < this.trails.length; i++) {\n const trail = this.trails[i], mesh = this.meshes[i];\n mesh.visible = STATE.particleTrailsEnabled && i < this.trails.length * STATE.qualityLevel;\n if (!mesh.visible) continue;\n\n const angle = time * 0.5 * trail.speed + trail.phase;\n const newPos = this.nextPosition.set(\n trail.anchor.x * halfWidth + Math.cos(angle) * (4 + audio.bass * 8),\n trail.anchor.y * halfHeight + Math.sin(time * 0.3 + trail.phase) * (4 + audio.mid * 8),\n Math.sin(angle) * (4 + audio.high * 7)\n );\n newPos.x = THREE.MathUtils.clamp(newPos.x, -halfWidth * 0.94, halfWidth * 0.94);\n newPos.y = THREE.MathUtils.clamp(newPos.y, -halfHeight * 0.94, halfHeight * 0.94);\n\n for (let j = trail.positions.length - 1; j > 0; j--) trail.positions[j].copy(trail.positions[j - 1]);\n trail.positions[0].copy(newPos);\n\n const posAttr = mesh.geometry.getAttribute('position');\n for (let j = 0; j < trail.positions.length; j++) {\n posAttr.setXYZ(j, trail.positions[j].x, trail.positions[j].y, trail.positions[j].z);\n }\n posAttr.needsUpdate = true;\n\n mesh.material.uniforms.uBeat.value = audio.beat;\n mesh.material.uniforms.uOpacity.value = STATE.particleTrailOpacity;\n }\n }\n }\n\n const trailSystem = new TrailSystem(50, 12);\n\n // \u2550\u2550\u2550\u2550\u2550\u2550\u2550 PARTICLE CONNECTIONS (Force Network) \u2550\u2550\u2550\u2550\u2550\u2550\u2550\n const connectionCount = 500; // Number of particles to check for connections\n const maxConnections = 800; // Maximum line segments\n const connectionGeometry = new THREE.BufferGeometry();\n const connectionPositions = new Float32Array(maxConnections * 6); // 2 points per line, 3 coords each\n const connectionAlphas = new Float32Array(maxConnections * 2); // Alpha per vertex\n connectionGeometry.setAttribute('position', new THREE.BufferAttribute(connectionPositions, 3));\n connectionGeometry.setAttribute('alpha', new THREE.BufferAttribute(connectionAlphas, 1));\n connectionGeometry.setDrawRange(0, 0);\n\n const connectionMaterial = new THREE.ShaderMaterial({\n vertexShader: `\n attribute float alpha;\n varying float vAlpha;\n void main() {\n vAlpha = alpha;\n vec4 mvPos = modelViewMatrix * vec4(position, 1.0);\n gl_Position = projectionMatrix * mvPos;\n }\n `,\n fragmentShader: `\n uniform vec3 uColor;\n uniform vec3 uColor2;\n uniform float uOpacity;\n uniform float uBeatEnergy;\n uniform float uHighMid;\n uniform float uSpectralCentroid;\n varying float vAlpha;\n void main() {\n // Blend hue with spectral centroid (brighter/higher = more c2)\n vec3 color = mix(uColor, uColor2, clamp(uSpectralCentroid * 2.0, 0.0, 1.0));\n // Beat pulse flash\n float beatFlash = 1.0 + uBeatEnergy * 1.2;\n color *= beatFlash;\n // Per-line alpha reacts to highMid (attack/presence energy)\n float lineAlpha = vAlpha * uOpacity * (0.4 + uHighMid * 0.5 + uBeatEnergy * 0.4);\n gl_FragColor = vec4(color, clamp(lineAlpha, 0.0, 1.0));\n }\n `,\n uniforms: {\n uColor: { value: new THREE.Color(def.c2) },\n uColor2: { value: new THREE.Color(def.c1) },\n uOpacity: { value: 1.0 },\n uBeatEnergy: { value: 0 },\n uHighMid: { value: 0 },\n uSpectralCentroid: { value: 0 }\n },\n transparent: true,\n depthWrite: false,\n blending: THREE.AdditiveBlending\n });\n\n const connectionMesh = new THREE.LineSegments(connectionGeometry, connectionMaterial);\n scene.add(connectionMesh);\n\n // Particle indices to track for connections \u2014 refreshed periodically\n const trackedParticles = [];\n const positions = Array.from({ length: connectionCount }, () => new THREE.Vector3());\n let connectionRefreshCounter = 0;\n const CONNECTION_REFRESH_INTERVAL = 40; // 20 Hz updates preserve the original ~2 s selection cycle\n\n function refreshTrackedParticles() {\n trackedParticles.length = 0;\n const visibleParticles = Math.max(1, geometry.drawRange.count);\n const visibleFraction = Math.sqrt(visibleParticles / (CONFIG.maxParticles * CONFIG.initialDensity));\n const trackedCount = Math.max(2, Math.floor(connectionCount * Math.min(1, STATE.qualityLevel, visibleFraction)));\n for (let i = 0; i < trackedCount; i++) {\n trackedParticles.push(Math.floor(Math.random() * visibleParticles));\n }\n }\n refreshTrackedParticles();\n let lastConnectionState = null;\n\n function shouldRebuildConnections(activeAudio, current, previous, idleElapsed) {\n return activeAudio || idleElapsed >= 2 || !previous || current.some((value, index) => value !== previous[index]);\n }\n\n function updateConnections(threshold, bassBoost, highMid, mid, forceRefresh = false) {\n // Refresh particle selection every N frames \u2014 network evolves over time\n connectionRefreshCounter++;\n if (forceRefresh || connectionRefreshCounter >= CONNECTION_REFRESH_INTERVAL) {\n refreshTrackedParticles();\n connectionRefreshCounter = 0;\n }\n\n // Mirror the shader's screen-fitted seed mapping for the line systems.\n for (let i = 0; i < trackedParticles.length; i++) {\n fieldParticlePosition(trackedParticles[i], positions[i]);\n }\n\n // Threshold breathes with bass, tightens on high energy\n const effectiveThreshold = threshold * (1.0 + bassBoost * 0.6) * (1.0 - highMid * 0.15);\n const thresholdSq = effectiveThreshold * effectiveThreshold;\n\n // Dynamic connection cap \u2014 expands with mid+highMid energy\n const dynamicMax = Math.floor(maxConnections * (0.3 + (mid + highMid) * 0.7));\n\n let connIdx = 0;\n for (let i = 0; i < trackedParticles.length && connIdx < dynamicMax; i++) {\n for (let j = i + 1; j < trackedParticles.length && connIdx < dynamicMax; j++) {\n const distSq = positions[i].distanceToSquared(positions[j]);\n if (distSq < thresholdSq && distSq > 0.1) {\n const dist = Math.sqrt(distSq);\n const alpha = (1.0 - dist / effectiveThreshold) * 0.8;\n\n connectionPositions[connIdx * 6 + 0] = positions[i].x;\n connectionPositions[connIdx * 6 + 1] = positions[i].y;\n connectionPositions[connIdx * 6 + 2] = positions[i].z;\n connectionPositions[connIdx * 6 + 3] = positions[j].x;\n connectionPositions[connIdx * 6 + 4] = positions[j].y;\n connectionPositions[connIdx * 6 + 5] = positions[j].z;\n\n connectionAlphas[connIdx * 2 + 0] = alpha;\n connectionAlphas[connIdx * 2 + 1] = alpha;\n\n connIdx++;\n }\n }\n }\n\n connectionGeometry.attributes.position.needsUpdate = true;\n connectionGeometry.attributes.alpha.needsUpdate = true;\n connectionGeometry.setDrawRange(0, connIdx * 2);\n }\n\n // \u2550\u2550\u2550\u2550\u2550\u2550\u2550 NETWORK CRAWLERS \u2550\u2550\u2550\u2550\u2550\u2550\u2550\n const CRAWLER_COUNT = 24; // concurrent crawlers\n const CRAWLER_TAIL = 10; // trail positions per crawler\n const crawlerMaxSegs = CRAWLER_COUNT * (CRAWLER_TAIL - 1);\n\n const crawlerGeo = new THREE.BufferGeometry();\n const crawlerPos = new Float32Array(crawlerMaxSegs * 6);\n const crawlerAlp = new Float32Array(crawlerMaxSegs * 2);\n crawlerGeo.setAttribute('position', new THREE.BufferAttribute(crawlerPos, 3));\n crawlerGeo.setAttribute('alpha', new THREE.BufferAttribute(crawlerAlp, 1));\n crawlerGeo.setDrawRange(0, 0);\n\n // Bloom only lights pixels above 0.85 luminance. Preset greens and cyans got there on their own;\n // a theme's reds and violets carry the same energy at a fraction of the luminance, so reaction\n // colours keep the theme's hue but are lifted (past 1.0 if needed) to the brightness of the originals.\n function glowColor(color, luminance) {\n const current = 0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b;\n return color.multiplyScalar(Math.min(4, Math.max(1, luminance / Math.max(current, 0.02))));\n }\n const crawlerLow = new THREE.Color(def.c1), crawlerHigh = new THREE.Color(def.c3);\n const crawlerTarget = new THREE.Color();\n const crawlerMaterial = new THREE.ShaderMaterial({\n vertexShader: `\n attribute float alpha;\n varying float vAlpha;\n void main() {\n vAlpha = alpha;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n }\n `,\n fragmentShader: `\n uniform vec3 uColor;\n uniform float uBeatEnergy;\n varying float vAlpha;\n void main() {\n vec3 c = uColor * (1.2 + uBeatEnergy * 1.5);\n gl_FragColor = vec4(c, vAlpha);\n }\n `,\n uniforms: {\n uColor: { value: new THREE.Color(def.c1) },\n uBeatEnergy: { value: 0 }\n },\n transparent: true,\n depthWrite: false,\n blending: THREE.AdditiveBlending\n });\n\n const crawlerMesh = new THREE.LineSegments(crawlerGeo, crawlerMaterial);\n scene.add(crawlerMesh);\n\n // Initialise crawler agents\n const crawlers = [];\n const CRAWLER_MIN_STEP = 1.8; // min world-space distance before adding a tail point\n for (let i = 0; i < CRAWLER_COUNT; i++) {\n crawlers.push({\n fromIdx: Math.floor(Math.random() * connectionCount),\n toIdx: Math.floor(Math.random() * connectionCount),\n t: Math.random(),\n speed: 0.0015 + Math.random() * 0.002,\n tail: [],\n lastTailPos: null, // last world position a tail point was recorded\n hue: Math.random()\n });\n }\n\n function updateCrawlers(dt, highMid, beatEnergy, spectralCentroid) {\n // Helper: world position of a tracked particle\n function particlePos(idx) {\n return positions[idx % trackedParticles.length];\n }\n\n // Helper: find nearest neighbour within threshold\n function nearestNeighbour(fromIdx, excludeIdx) {\n const origin = particlePos(fromIdx);\n const threshold = STATE.connectionThreshold * (1.2 + highMid * 0.4);\n const threshSq = threshold * threshold;\n let bestDist = Infinity, bestIdx = -1;\n const check = Math.min(trackedParticles.length, 80); // sample subset for perf\n const start = Math.floor(Math.random() * (trackedParticles.length - check));\n for (let k = start; k < start + check; k++) {\n const ki = k % trackedParticles.length;\n if (ki === fromIdx || ki === excludeIdx) continue;\n const d = origin.distanceToSquared(particlePos(ki));\n if (d < threshSq && d < bestDist) { bestDist = d; bestIdx = ki; }\n }\n // fallback: pick random if no neighbour in range\n return bestIdx === -1 ? Math.floor(Math.random() * trackedParticles.length) : bestIdx;\n }\n\n let segIdx = 0;\n\n for (let i = 0; i < crawlers.length; i++) {\n const c = crawlers[i];\n\n // Speed reacts to highMid (attack energy) and beat\n const spd = c.speed * (1.0 + highMid * 0.8 + beatEnergy * 0.5);\n c.t += spd;\n\n // Reached target \u2014 advance to next node\n if (c.t >= 1.0) {\n c.t -= 1.0;\n c.fromIdx = c.toIdx;\n c.toIdx = nearestNeighbour(c.fromIdx, c.fromIdx);\n // Occasionally teleport to keep crawlers spread across the field\n if (Math.random() < 0.04) {\n c.fromIdx = Math.floor(Math.random() * trackedParticles.length);\n c.toIdx = nearestNeighbour(c.fromIdx, c.fromIdx);\n c.tail = [];\n c.lastTailPos = null;\n }\n }\n\n // Current head position\n const from = particlePos(c.fromIdx);\n const to = particlePos(c.toIdx);\n const head = new THREE.Vector3().lerpVectors(from, to, c.t);\n\n // Only record a tail point when head has moved far enough in world space\n const distMoved = c.lastTailPos ? head.distanceTo(c.lastTailPos) : Infinity;\n if (distMoved >= CRAWLER_MIN_STEP) {\n c.tail.push(head.clone());\n if (c.tail.length > CRAWLER_TAIL) c.tail.shift();\n c.lastTailPos = head.clone();\n }\n\n // Write tail segments\n const tailLen = c.tail.length;\n for (let s = 0; s < tailLen - 1 && segIdx < crawlerMaxSegs; s++) {\n const p0 = c.tail[s];\n const p1 = c.tail[s + 1];\n // Alpha: head is brightest, tail fades\n const baseAlpha = ((s + 1) / (tailLen - 1));\n const alpha = baseAlpha * (0.25 + spectralCentroid * 0.3 + beatEnergy * 0.4);\n\n crawlerPos[segIdx * 6 + 0] = p0.x; crawlerPos[segIdx * 6 + 1] = p0.y; crawlerPos[segIdx * 6 + 2] = p0.z;\n crawlerPos[segIdx * 6 + 3] = p1.x; crawlerPos[segIdx * 6 + 4] = p1.y; crawlerPos[segIdx * 6 + 5] = p1.z;\n crawlerAlp[segIdx * 2 + 0] = alpha;\n crawlerAlp[segIdx * 2 + 1] = alpha;\n segIdx++;\n }\n }\n\n crawlerGeo.attributes.position.needsUpdate = true;\n crawlerGeo.attributes.alpha.needsUpdate = true;\n crawlerGeo.setDrawRange(0, segIdx * 2);\n }\n\n STATE.crawlersEnabled = true;\n\n // \u2550\u2550\u2550\u2550\u2550\u2550\u2550 NEBULA BACKGROUND \u2550\u2550\u2550\u2550\u2550\u2550\u2550\n const nebulaVertexShader = `\nvarying vec3 vWorldPosition;\nvarying vec2 vUv;\nvoid main() {\n vUv = uv;\n vec4 worldPos = modelMatrix * vec4(position, 1.0);\n vWorldPosition = worldPos.xyz;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n}\n`;\n\n const nebulaFragmentShader = `\nuniform float uTime;\nuniform vec3 uColor1;\nuniform vec3 uColor2;\nuniform vec3 uColor3;\nuniform float uBass;\nuniform float uMid;\nuniform float uHigh;\nuniform float uBeatEnergy;\nuniform float uIntensity;\nvarying vec3 vWorldPosition;\nvarying vec2 vUv;\n\n// Simplex 3D noise\nvec4 permute(vec4 x) { return mod(((x*34.0)+1.0)*x, 289.0); }\nvec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; }\n\nfloat snoise(vec3 v) {\n const vec2 C = vec2(1.0/6.0, 1.0/3.0);\n const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);\n vec3 i = floor(v + dot(v, C.yyy));\n vec3 x0 = v - i + dot(i, C.xxx);\n vec3 g = step(x0.yzx, x0.xyz);\n vec3 l = 1.0 - g;\n vec3 i1 = min(g.xyz, l.zxy);\n vec3 i2 = max(g.xyz, l.zxy);\n vec3 x1 = x0 - i1 + C.xxx;\n vec3 x2 = x0 - i2 + C.yyy;\n vec3 x3 = x0 - D.yyy;\n i = mod(i, 289.0);\n 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));\n float n_ = 1.0/7.0;\n vec3 ns = n_ * D.wyz - D.xzx;\n vec4 j = p - 49.0 * floor(p * ns.z * ns.z);\n vec4 x_ = floor(j * ns.z);\n vec4 y_ = floor(j - 7.0 * x_);\n vec4 x = x_ *ns.x + ns.yyyy;\n vec4 y = y_ *ns.x + ns.yyyy;\n vec4 h = 1.0 - abs(x) - abs(y);\n vec4 b0 = vec4(x.xy, y.xy);\n vec4 b1 = vec4(x.zw, y.zw);\n vec4 s0 = floor(b0)*2.0 + 1.0;\n vec4 s1 = floor(b1)*2.0 + 1.0;\n vec4 sh = -step(h, vec4(0.0));\n vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy;\n vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww;\n vec3 p0 = vec3(a0.xy, h.x);\n vec3 p1 = vec3(a0.zw, h.y);\n vec3 p2 = vec3(a1.xy, h.z);\n vec3 p3 = vec3(a1.zw, h.w);\n vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2,p2), dot(p3,p3)));\n p0 *= norm.x; p1 *= norm.y; p2 *= norm.z; p3 *= norm.w;\n vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);\n return 42.0 * dot(m*m*m*m, vec4(dot(p0,x0), dot(p1,x1), dot(p2,x2), dot(p3,x3)));\n}\n\nfloat fbm(vec3 p) {\n float value = 0.0;\n float amplitude = 0.5;\n float frequency = 1.0;\n for(int i = 0; i < 3; i++) {\n value += amplitude * snoise(p * frequency);\n amplitude *= 0.5;\n frequency *= 2.0;\n }\n return value;\n}\n\nvoid main() {\n vec3 dir = normalize(vWorldPosition);\n float t = uTime * 0.02;\n\n // Layered nebula clouds\n vec3 p1 = dir * 2.0 + t * 0.5;\n vec3 p2 = dir * 4.0 - t * 0.3;\n vec3 p3 = dir * 1.5 + vec3(t * 0.2, -t * 0.1, t * 0.15);\n\n float n1 = fbm(p1) * 0.5 + 0.5;\n float n2 = fbm(p2) * 0.5 + 0.5;\n float n3 = fbm(p3) * 0.5 + 0.5;\n\n // Create cloud density\n float density = n1 * n2;\n density = pow(density, 1.5) * 1.5;\n\n // Wispy tendrils\n float tendrils = pow(n3, 3.0) * 0.8;\n\n // Color mixing based on field colors\n vec3 nebulaColor = mix(uColor1 * 0.3, uColor2 * 0.5, n1);\n nebulaColor = mix(nebulaColor, uColor3 * 0.4, n2 * 0.5);\n\n // Add some complementary deep space colors\n vec3 deepSpace = vec3(0.02, 0.01, 0.05);\n vec3 starGlow = vec3(0.1, 0.08, 0.15);\n\n // Audio reactivity\n float bassGlow = uBass * 0.3;\n float midShimmer = uMid * 0.2 * sin(uTime * 2.0 + n1 * 10.0);\n float beatPulse = uBeatEnergy * 0.4;\n\n // Combine layers\n vec3 color = deepSpace;\n color += nebulaColor * density * (0.4 + bassGlow + beatPulse);\n color += uColor2 * 0.15 * tendrils * (1.0 + uHigh * 0.5);\n color += starGlow * pow(n1 * n2, 4.0) * (1.0 + midShimmer);\n\n // Scattered stars\n float stars = pow(snoise(dir * 50.0) * 0.5 + 0.5, 20.0);\n stars += pow(snoise(dir * 80.0 + 100.0) * 0.5 + 0.5, 25.0) * 0.5;\n color += vec3(1.0, 0.95, 0.9) * stars * (0.5 + uHigh * 0.5);\n\n // Vignette toward horizon\n float horizonFade = 1.0 - pow(abs(dir.y), 0.5) * 0.3;\n color *= horizonFade;\n\n // Apply intensity\n color *= uIntensity;\n\n gl_FragColor = vec4(color, 1.0);\n}\n`;\n\n const nebulaMaterial = new THREE.ShaderMaterial({\n vertexShader: nebulaVertexShader,\n fragmentShader: nebulaFragmentShader,\n uniforms: {\n uTime: { value: 0 },\n uColor1: { value: new THREE.Color(def.c1) },\n uColor2: { value: new THREE.Color(def.c2) },\n uColor3: { value: new THREE.Color(def.c3) },\n uBass: { value: 0 },\n uMid: { value: 0 },\n uHigh: { value: 0 },\n uBeatEnergy: { value: 0 },\n uIntensity: { value: STATE.nebulaIntensity }\n },\n side: THREE.BackSide,\n depthWrite: false\n });\n\n const nebulaSphere = new THREE.Mesh(\n new THREE.SphereGeometry(500, 64, 64),\n nebulaMaterial\n );\n scene.add(nebulaSphere);\n\n // \u2550\u2550\u2550\u2550\u2550\u2550\u2550 POST-PROCESSING COMPOSER \u2550\u2550\u2550\u2550\u2550\u2550\u2550\n const composer = new EffectComposer(renderer);\n composer.addPass(new RenderPass(scene, camera));\n\n const afterimagePass = new AfterimagePass();\n afterimagePass.uniforms['damp'].value = STATE.trails;\n composer.addPass(afterimagePass);\n\n const bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5, 0.4, 0.85);\n bloomPass.threshold = 0.25;\n bloomPass.strength = STATE.bloom;\n bloomPass.radius = 0.45;\n composer.addPass(bloomPass);\n\n const godRaysPass = new ShaderPass(GodRaysShader);\n godRaysPass.enabled = false;\n composer.addPass(godRaysPass);\n\n const lensFlarePass = new ShaderPass(LensFlareShader);\n lensFlarePass.enabled = STATE.lensFlareEnabled && STATE.lensFlareIntensity > 0;\n composer.addPass(lensFlarePass);\n\n const anamorphicPass = new ShaderPass(AnamorphicShader);\n composer.addPass(anamorphicPass);\n\n const chromaticPass = new ShaderPass(ChromaticShader);\n chromaticPass.enabled = STATE.chromaticAberration > 0;\n composer.addPass(chromaticPass);\n\n // Custom DOF pass (radial focus)\n const customDOFPass = new ShaderPass(CustomDOFShader);\n customDOFPass.uniforms.uFocus.value = STATE.dofFocus;\n customDOFPass.uniforms.uFocalLength.value = STATE.dofFocalLength;\n customDOFPass.uniforms.uBokehStrength.value = STATE.dofBokehStrength;\n customDOFPass.uniforms.uResolution.value.set(window.innerWidth, window.innerHeight);\n customDOFPass.enabled = STATE.dofEnabled;\n composer.addPass(customDOFPass);\n\n const vignettePass = new ShaderPass(VignetteShader);\n composer.addPass(vignettePass);\n\n const grainPass = new ShaderPass(GrainShader);\n grainPass.enabled = STATE.filmGrain > 0;\n composer.addPass(grainPass);\n\n const AlphaFromLumaShader = {\n uniforms: { tDiffuse: { value: null } },\n vertexShader: `varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`,\n fragmentShader: `uniform sampler2D tDiffuse; varying vec2 vUv;\n void main() {\n vec3 rgb = texture2D(tDiffuse, vUv).rgb;\n float luminance = max(max(rgb.r, rgb.g), rgb.b);\n float alpha = smoothstep(0.004, 0.06, luminance);\n gl_FragColor = vec4(rgb, alpha);\n }`\n };\n composer.addPass(new OutputPass());\n composer.addPass(new ShaderPass(AlphaFromLumaShader));\n\n // Keep the embedded CSS accents aligned with the kit palette.\n function applyCssTheme(accent) {\n const luminance = accent.r * 0.299 + accent.g * 0.587 + accent.b * 0.114;\n const boost = luminance < 0.35 ? 0.35 / Math.max(luminance, 0.001) : 1;\n for (const [channel, value] of Object.entries({ ar: accent.r, ag: accent.g, ab: accent.b })) {\n document.documentElement.style.setProperty(`--${channel}`, Math.min(Math.round(value * boost * 255), 255));\n }\n }\n applyCssTheme(new THREE.Color(def.c1));\n\n // \u2550\u2550\u2550\u2550\u2550\u2550\u2550 UI & EVENTS \u2550\u2550\u2550\u2550\u2550\u2550\u2550\n const overlay = document.getElementById('overlay');\n const audioControls = document.getElementById('audio-controls');\n const playPauseBtn = document.getElementById('play-pause-btn');\n const progressBar = document.getElementById('progress-bar');\n const progressContainer = document.getElementById('progress-container');\n const timeDisplay = document.getElementById('time-display');\n const trackName = document.getElementById('track-name');\n const status = document.getElementById('status-bar');\n\n const formatTime = s => `${Math.floor(s / 60)}:${Math.floor(s % 60).toString().padStart(2, '0')}`;\n\n function updateUI() {\n if (AUDIO.mode === 'mic') {\n playPauseBtn.textContent = '\u{1F399} live';\n playPauseBtn.disabled = true;\n } else {\n playPauseBtn.textContent = AUDIO.isPlaying ? '\u23F8 pause' : '\u25B6 play';\n playPauseBtn.classList.toggle('playing', AUDIO.isPlaying);\n playPauseBtn.disabled = false;\n }\n if (AUDIO.mode === 'file' && AUDIO.audioBuffer) {\n const cur = AUDIO.getCurrentTime(), dur = AUDIO.getDuration();\n progressBar.style.width = (cur / dur * 100) + '%';\n timeDisplay.textContent = `${formatTime(cur)} / ${formatTime(dur)}`;\n } else {\n progressBar.style.width = '0%';\n timeDisplay.textContent = AUDIO.mode === 'mic' ? 'LIVE' : '0:00 / 0:00';\n }\n }\n\n function togglePlayPause() {\n if (AUDIO.mode !== 'file') return;\n AUDIO.togglePlayPause();\n updateUI();\n status.innerHTML = AUDIO.isPlaying ? 'SYSTEM: <span>PLAYING</span>' : 'SYSTEM: <span>PAUSED</span>';\n }\n\n function activate() {\n AUDIO.init();\n if (AUDIO.ctx.state === 'suspended') AUDIO.ctx.resume();\n overlay.style.opacity = 0;\n setTimeout(() => overlay.style.display = 'none', 800);\n audioControls.classList.add('visible');\n }\n\n async function loadTrack(file) {\n activate();\n const buffer = await AUDIO.loadTrack(await file.arrayBuffer(), file.name);\n const samples = buffer.getChannelData(0);\n const bars = 160;\n const peaks = Array.from({ length: bars }, (_, bar) => {\n const from = Math.floor(bar * samples.length / bars);\n const to = Math.floor((bar + 1) * samples.length / bars);\n const stride = Math.max(1, Math.floor((to - from) / 64));\n let peak = 0;\n for (let i = from; i < to; i += stride) peak = Math.max(peak, Math.abs(samples[i]));\n return peak;\n });\n parent.postMessage({ type: 'ad-qf-waveform', value: peaks }, '*');\n AUDIO.play();\n trackName.textContent = file.name.length > 25 ? file.name.slice(0, 22) + '...' : file.name;\n updateUI();\n status.innerHTML = 'SYSTEM: <span>PLAYING</span>';\n }\n\n async function startMic(preferredLabel = null) {\n console.log('Global startMic requested:', preferredLabel);\n try {\n activate();\n await AUDIO.startMic(preferredLabel);\n trackName.textContent = AUDIO.currentTrackName;\n updateUI();\n // status update handled in AUDIO.startMic\n } catch (e) {\n console.error(\"Global startMic failed:\", e);\n const status = document.getElementById('status-bar');\n status.innerHTML = `ERROR: <span style=\"color:#ff6b6b\">${e.message}</span>`;\n }\n }\n\n // Using optional chaining/checks because one input was replaced by a button\n const enterBtn = document.getElementById('enter-btn');\n if (enterBtn) enterBtn.onclick = () => startMic(null);\n\n const fileInput = document.getElementById('file-input');\n if (fileInput) fileInput.onchange = e => e.target.files[0] && loadTrack(e.target.files[0]);\n\n document.getElementById('file-input-2').onchange = e => { if (e.target.files[0]) loadTrack(e.target.files[0]); e.target.value = ''; };\n document.getElementById('mic-btn-2').onclick = () => startMic(null);\n playPauseBtn.onclick = togglePlayPause;\n progressContainer.onclick = e => {\n if (AUDIO.mode !== 'file' || !AUDIO.audioBuffer) return;\n const rect = progressContainer.getBoundingClientRect();\n AUDIO.seek((e.clientX - rect.left) / rect.width * AUDIO.getDuration());\n };\n document.getElementById('screenshot-btn')?.addEventListener('click', () => {\n composer.render();\n const a = document.createElement('a');\n a.download = 'quantum-' + Date.now() + '.png';\n a.href = renderer.domElement.toDataURL('image/png');\n a.click();\n });\n document.getElementById('fullscreen-btn')?.addEventListener('click', () => {\n document.fullscreenElement ? document.exitFullscreen() : document.documentElement.requestFullscreen();\n });\n\n // \u2550\u2550\u2550\u2550\u2550\u2550\u2550 ANIMATION LOOP \u2550\u2550\u2550\u2550\u2550\u2550\u2550\n let renderVisible = true;\n let connectionElapsed = 1 / 20;\n let idleConnectionElapsed = 2;\n let frameRequest = 0;\n let nextFrameAt = 0;\n let lastRenderedAt = -1;\n let elapsed = 0;\n\n function startFrames() {\n if (!frameRequest && renderVisible && !document.hidden) frameRequest = requestAnimationFrame(animate);\n }\n\n function stopFrames() {\n if (frameRequest) cancelAnimationFrame(frameRequest);\n frameRequest = 0;\n nextFrameAt = 0;\n lastRenderedAt = -1;\n }\n\n document.addEventListener('visibilitychange', () => {\n if (document.hidden) stopFrames();\n else startFrames();\n });\n\n function animate(now) {\n frameRequest = 0;\n if (!renderVisible || document.hidden) return;\n startFrames();\n const frameInterval = 1000 / Math.max(1, STATE.targetFPS);\n if (now < nextFrameAt) return;\n nextFrameAt = Math.max(nextFrameAt + frameInterval, now);\n const dt = lastRenderedAt < 0 ? 0 : Math.min((now - lastRenderedAt) / 1000, 0.05);\n lastRenderedAt = now;\n elapsed += dt;\n\n material.uniforms.uTime.value += dt * STATE.timeScale;\n grainPass.uniforms.uTime.value = elapsed;\n lensFlarePass.uniforms.uTime.value = elapsed;\n\n material.uniforms.uZoomFactor.value = camera.position.length() / CONFIG.defaultZoom;\n material.uniforms.uMousePos.value.lerp(mouseWorldPos, 0.1);\n material.uniforms.uMouseVelocity.value.lerp(mouseVelocity, 0.08);\n\n AUDIO.update();\n\n const s = STATE.sensitivity, lf = 0.16;\n material.uniforms.uSubBass.value = THREE.MathUtils.lerp(material.uniforms.uSubBass.value, AUDIO.gatedBands.subBass * s, lf);\n material.uniforms.uBass.value = THREE.MathUtils.lerp(material.uniforms.uBass.value, AUDIO.gatedBands.bass * s, lf);\n material.uniforms.uLowMid.value = THREE.MathUtils.lerp(material.uniforms.uLowMid.value, AUDIO.gatedBands.lowMid * s, lf);\n material.uniforms.uMid.value = THREE.MathUtils.lerp(material.uniforms.uMid.value, AUDIO.gatedBands.mid * s, lf);\n material.uniforms.uHighMid.value = THREE.MathUtils.lerp(material.uniforms.uHighMid.value, AUDIO.gatedBands.highMid * s, lf);\n material.uniforms.uHigh.value = THREE.MathUtils.lerp(material.uniforms.uHigh.value, AUDIO.gatedBands.high * s, lf);\n material.uniforms.uUltraHigh.value = THREE.MathUtils.lerp(material.uniforms.uUltraHigh.value, AUDIO.gatedBands.ultraHigh * s, lf);\n material.uniforms.uBeatEnergy.value = THREE.MathUtils.lerp(material.uniforms.uBeatEnergy.value, AUDIO.beatEnergy, 0.22);\n material.uniforms.uSpectralCentroid.value = THREE.MathUtils.lerp(material.uniforms.uSpectralCentroid.value, AUDIO.spectralCentroid, lf);\n material.uniforms.uSpectralFlux.value = THREE.MathUtils.lerp(material.uniforms.uSpectralFlux.value, AUDIO.spectralFlux * s, lf);\n\n // === ENHANCEMENT: Update new uniforms ===\n material.uniforms.uOnsetEnergy.value = THREE.MathUtils.lerp(material.uniforms.uOnsetEnergy.value, AUDIO.onsetEnergy, 0.25);\n material.uniforms.uTerrainMode.value = FIELD_TYPES[STATE.field]?.terrain ? 1.0 : 0.0;\n material.uniforms.uTerrainHeight.value = STATE.waveformTerrainHeight;\n\n // Update secondary particles\n secondaryPoints.visible = STATE.secondaryEnabled;\n secondaryMaterial.uniforms.uTime.value = material.uniforms.uTime.value;\n secondaryMaterial.uniforms.uSubBass.value = material.uniforms.uSubBass.value;\n secondaryMaterial.uniforms.uBass.value = material.uniforms.uBass.value;\n secondaryMaterial.uniforms.uLowMid.value = material.uniforms.uLowMid.value;\n secondaryMaterial.uniforms.uMid.value = material.uniforms.uMid.value;\n secondaryMaterial.uniforms.uHighMid.value = material.uniforms.uHighMid.value;\n secondaryMaterial.uniforms.uHigh.value = material.uniforms.uHigh.value;\n secondaryMaterial.uniforms.uUltraHigh.value = material.uniforms.uUltraHigh.value;\n secondaryMaterial.uniforms.uBeatEnergy.value = material.uniforms.uBeatEnergy.value;\n secondaryMaterial.uniforms.uSpectralCentroid.value = material.uniforms.uSpectralCentroid.value;\n secondaryMaterial.uniforms.uSpectralFlux.value = material.uniforms.uSpectralFlux.value;\n secondaryMaterial.uniforms.uOnsetEnergy.value = material.uniforms.uOnsetEnergy.value;\n secondaryMaterial.uniforms.uVortexStrength.value = STATE.vortexStrength;\n\n // Update nebula background\n nebulaSphere.visible = STATE.nebulaEnabled;\n if (STATE.nebulaEnabled) {\n nebulaMaterial.uniforms.uTime.value = material.uniforms.uTime.value;\n nebulaMaterial.uniforms.uBass.value = material.uniforms.uBass.value;\n nebulaMaterial.uniforms.uMid.value = material.uniforms.uMid.value;\n nebulaMaterial.uniforms.uHigh.value = material.uniforms.uHigh.value;\n nebulaMaterial.uniforms.uBeatEnergy.value = material.uniforms.uBeatEnergy.value;\n }\n\n // Update particle connections\n connectionMesh.visible = STATE.connectionsEnabled;\n if (STATE.connectionsEnabled) {\n const activeAudio = AUDIO.mode === 'mic' || AUDIO.isPlaying || AUDIO.externalActive;\n const connectionState = [\n STATE.field, STATE.connectionThreshold, geometry.drawRange.count,\n material.uniforms.uViewportAspect.value, material.uniforms.uViewportHalfHeight.value, STATE.fieldSize,\n STATE.qualityLevel, activeAudio\n ];\n connectionElapsed = Math.min(1 / 20, connectionElapsed + dt);\n idleConnectionElapsed = Math.min(2, idleConnectionElapsed + dt);\n if (connectionElapsed >= 1 / 20 && shouldRebuildConnections(activeAudio, connectionState, lastConnectionState, idleConnectionElapsed)) {\n const refreshSelection = !lastConnectionState || connectionState[2] !== lastConnectionState[2]\n || connectionState[6] !== lastConnectionState[6] || (!activeAudio && idleConnectionElapsed >= 2);\n updateConnections(\n STATE.connectionThreshold,\n activeAudio ? material.uniforms.uBass.value : 0,\n activeAudio ? material.uniforms.uHighMid.value : 0,\n activeAudio ? material.uniforms.uMid.value : 0,\n refreshSelection\n );\n lastConnectionState = connectionState;\n connectionElapsed = 0;\n idleConnectionElapsed = 0;\n }\n connectionMaterial.uniforms.uBeatEnergy.value = material.uniforms.uBeatEnergy.value;\n connectionMaterial.uniforms.uHighMid.value = material.uniforms.uHighMid.value;\n connectionMaterial.uniforms.uSpectralCentroid.value = material.uniforms.uSpectralCentroid.value;\n connectionMaterial.uniforms.uOpacity.value = STATE.connectionOpacity;\n } else {\n connectionElapsed = 1 / 20;\n idleConnectionElapsed = 2;\n lastConnectionState = null;\n }\n\n // Update crawlers\n crawlerMesh.visible = STATE.crawlersEnabled && STATE.connectionsEnabled && (AUDIO.externalActive || AUDIO.active && (AUDIO.isPlaying || AUDIO.mode === 'mic'));\n if (crawlerMesh.visible) {\n updateCrawlers(\n dt,\n material.uniforms.uHighMid.value,\n material.uniforms.uBeatEnergy.value,\n material.uniforms.uSpectralCentroid.value\n );\n crawlerTarget.lerpColors(crawlerLow, crawlerHigh, material.uniforms.uSpectralCentroid.value);\n crawlerMaterial.uniforms.uColor.value.lerp(crawlerTarget, 0.05);\n crawlerMaterial.uniforms.uBeatEnergy.value = material.uniforms.uBeatEnergy.value;\n }\n\n // Update post-processing\n chromaticPass.uniforms.uBeatEnergy.value = material.uniforms.uBeatEnergy.value;\n godRaysPass.uniforms.uBeatEnergy.value = material.uniforms.uBeatEnergy.value;\n lensFlarePass.uniforms.uBeatEnergy.value = material.uniforms.uBeatEnergy.value;\n vignettePass.uniforms.uBeatEnergy.value = material.uniforms.uBeatEnergy.value;\n customDOFPass.uniforms.uBeatEnergy.value = material.uniforms.uBeatEnergy.value;\n\n\n\n // Update trails\n trailSystem.update(elapsed, {\n bass: material.uniforms.uBass.value,\n mid: material.uniforms.uMid.value,\n high: material.uniforms.uHigh.value,\n beat: material.uniforms.uBeatEnergy.value\n }, material.uniforms.uColor1.value);\n\n // === ENHANCEMENT: Adaptive Quality System ===\n QUALITY_SYSTEM.update(dt * 1000);\n\n // === ENHANCEMENT: Audio-Reactive Camera ===\n AUDIO_CAMERA.update(dt * 1000);\n\n // Camera shake\n if (STATE.cameraShake > 0 && AUDIO.beatEnergy > 0.1) {\n const shake = AUDIO.beatEnergy * STATE.cameraShake;\n cameraShakeOffset.set((Math.random() - 0.5) * shake * 0.6, (Math.random() - 0.5) * shake * 0.4, (Math.random() - 0.5) * shake * 0.25);\n camera.position.copy(baseCameraPos).add(cameraShakeOffset);\n }\n\n controls.autoRotateSpeed = 0.35 + material.uniforms.uMid.value * 2.0 + material.uniforms.uBeatEnergy.value * 2.0;\n bloomPass.strength = STATE.bloom + (AUDIO.gatedBands.bass + AUDIO.gatedBands.mid + AUDIO.beatEnergy) / 6;\n bloomPass.enabled = bloomPass.strength > 0;\n anamorphicPass.uniforms.uIntensity.value = STATE.anamorphicStretch + AUDIO.gatedBands.bass * 0.4;\n anamorphicPass.enabled = anamorphicPass.uniforms.uIntensity.value > 0;\n\n controls.update();\n composer.render();\n }\n\n window.addEventListener('resize', () => {\n camera.aspect = window.innerWidth / window.innerHeight;\n camera.updateProjectionMatrix();\n material.uniforms.uViewportAspect.value = window.innerWidth / window.innerHeight;\n secondaryMaterial.uniforms.uViewportAspect.value = material.uniforms.uViewportAspect.value;\n material.uniforms.uViewportHalfHeight.value = Math.tan(camera.fov * Math.PI / 360) * CONFIG.defaultZoom;\n secondaryMaterial.uniforms.uViewportHalfHeight.value = material.uniforms.uViewportHalfHeight.value;\n renderer.setSize(window.innerWidth, window.innerHeight);\n composer.setSize(window.innerWidth, window.innerHeight);\n STATE.pixelRatio = fitPixelRatio();\n QUALITY_SYSTEM.applyQuality();\n customDOFPass.uniforms.uResolution.value.set(window.innerWidth, window.innerHeight);\n baseCameraPos.copy(camera.position);\n });\n\n let pausedFromKit = false;\n let requestedTimeScale = STATE.timeScale;\n const sideEffects = {\n trails: value => afterimagePass.uniforms['damp'].value = value,\n chromaticAberration: value => { chromaticPass.uniforms.uIntensity.value = value; chromaticPass.enabled = value > 0; },\n filmGrain: value => { grainPass.uniforms.uIntensity.value = value; grainPass.enabled = value > 0; },\n nebulaIntensity: value => nebulaMaterial.uniforms.uIntensity.value = value,\n lensFlareEnabled: value => lensFlarePass.enabled = value && STATE.lensFlareIntensity > 0,\n lensFlareIntensity: value => { lensFlarePass.uniforms.uIntensity.value = value; lensFlarePass.enabled = STATE.lensFlareEnabled && value > 0; },\n dofEnabled: value => customDOFPass.enabled = value,\n dofFocus: value => customDOFPass.uniforms.uFocus.value = value,\n dofFocalLength: value => customDOFPass.uniforms.uFocalLength.value = value,\n dofBokehStrength: value => customDOFPass.uniforms.uBokehStrength.value = value,\n godRaysEnabled: value => godRaysPass.enabled = value && STATE.godRaysIntensity > 0,\n godRaysIntensity: value => { godRaysPass.uniforms.uIntensity.value = value; godRaysPass.enabled = STATE.godRaysEnabled && value > 0; },\n vortexStrength: value => material.uniforms.uVortexStrength.value = value,\n pulseIntensity: value => material.uniforms.uPulseIntensity.value = value,\n fieldSize: value => { material.uniforms.uFieldFill.value = value; secondaryMaterial.uniforms.uFieldFill.value = value; },\n };\n window.addEventListener('message', ({ source, data }) => {\n if (source !== parent || data?.type !== 'ad-qf-control') return;\n const { key, value } = data;\n if (key === 'field' && FIELD_TYPES[value]) {\n STATE.field = value;\n const selected = FIELD_TYPES[value];\n material.uniforms.uNoiseScale.value = selected.s;\n material.uniforms.uCurlStrength.value = selected.curl;\n material.uniforms.uSizeBase.value = selected.sz;\n } else if (key === 'visible') {\n renderVisible = !!value;\n if (renderVisible) startFrames();\n else stopFrames();\n }\n else if (key === 'interactive') controls.enabled = !!value;\n else if (key === 'spectrum') EXTERNAL.set(value?.bands, value?.range);\n else if (key === 'particleCount') {\n particleCount = Number.isFinite(value) ? Math.max(0, Math.floor(value)) : null;\n QUALITY_SYSTEM.applyQuality();\n }\n else if (key === 'particleBudget') {\n particleBudget = Number.isFinite(value) ? Math.max(0, Math.floor(value)) : Infinity;\n QUALITY_SYSTEM.applyQuality();\n }\n else if (key === 'timeScale') { requestedTimeScale = value; if (!pausedFromKit) STATE.timeScale = value; }\n else if (key === 'paused') { pausedFromKit = !!value; STATE.timeScale = pausedFromKit ? 0 : requestedTimeScale; }\n else if (key === 'play') togglePlayPause();\n else if (key === 'file' && value) loadTrack(value);\n else if (key === 'mic') {\n if (AUDIO.mode === 'mic') { AUDIO.stopCurrentSource(); AUDIO.mode = null; }\n else startMic(null);\n }\n else if (key === 'seek') { AUDIO.seek(value); updateUI(); }\n else if (key === 'screenshot') document.getElementById('screenshot-btn').click();\n else if (key === 'palette' && Array.isArray(value)) {\n const main = new THREE.Color(value[0]), light = new THREE.Color(value[1]);\n applyCssTheme(main);\n material.uniforms.uColor1.value.copy(main);\n material.uniforms.uColor2.value.copy(light);\n material.uniforms.uColor3.value.copy(light).lerp(new THREE.Color('#ffffff'), 0.32);\n secondaryMaterial.uniforms.uColor1.value.copy(main);\n secondaryMaterial.uniforms.uColor2.value.copy(light);\n nebulaMaterial.uniforms.uColor1.value.copy(main);\n nebulaMaterial.uniforms.uColor2.value.copy(light);\n nebulaMaterial.uniforms.uColor3.value.copy(main);\n connectionMaterial.uniforms.uColor.value.copy(light);\n connectionMaterial.uniforms.uColor2.value.copy(main);\n const white = new THREE.Color('#ffffff');\n const tint = glowColor(light.clone().lerp(white, 0.35), 0.75);\n lensFlarePass.uniforms.uTint.value.copy(tint);\n anamorphicPass.uniforms.uTint.value.copy(tint);\n // Reaction lines stand out from the field like the presets' contrasting colours did:\n // a luminous tint of the theme's light colour with a near-white core on bright passages.\n glowColor(crawlerLow.copy(light).lerp(white, 0.2), 0.75);\n glowColor(crawlerHigh.copy(light).lerp(white, 0.5), 0.95);\n crawlerMaterial.uniforms.uColor.value.copy(crawlerLow);\n trailSystem.trails.forEach((trail, i) => glowColor(trail.color.copy(main).lerp(light, i / Math.max(1, trailSystem.trails.length - 1)).lerp(white, 0.15), 0.6));\n } else if (Object.prototype.hasOwnProperty.call(STATE, key)) {\n STATE[key] = value;\n if (key === 'density') QUALITY_SYSTEM.applyQuality();\n sideEffects[key]?.(value);\n }\n });\n let lastTransport = '';\n setInterval(() => {\n if (!renderVisible || document.hidden) return;\n const value = {\n mode: AUDIO.mode, playing: AUDIO.isPlaying,\n current: AUDIO.mode === 'file' ? AUDIO.getCurrentTime() : 0,\n duration: AUDIO.mode === 'file' ? AUDIO.getDuration() : 0,\n track: AUDIO.mode ? AUDIO.currentTrackName : ''\n };\n const snapshot = JSON.stringify(value);\n if (snapshot !== lastTransport) {\n lastTransport = snapshot;\n parent.postMessage({ type: 'ad-qf-transport', value }, '*');\n }\n }, 250);\n parent.postMessage({ type: 'ad-qf-ready' }, '*');\n\n QUALITY_SYSTEM.applyQuality();\n startFrames();\n <\/script>\n</body>\n\n</html>\n";
5080
5080
 
5081
5081
  // src/components/artwork/QuantumField/QuantumFieldExperience.tsx
5082
5082
  import { jsx as jsx46, jsxs as jsxs37 } from "react/jsx-runtime";
5083
+ var quantumBackgroundThreeUrl = "https://unpkg.com/three@0.182.0/";
5084
+ var sceneDocument = (threeUrl) => {
5085
+ const base = threeUrl.endsWith("/") ? threeUrl : `${threeUrl}/`;
5086
+ return base === quantumBackgroundThreeUrl ? reference_default : reference_default.replaceAll(quantumBackgroundThreeUrl, base);
5087
+ };
5083
5088
  var quantumBackgroundFields = ["Photon (EM)", "Gluon (Strong)", "Higgs (Mass)", "Gravity", "Dark Energy", "Neutrino", "Waveform Terrain"];
5084
5089
  var quantumBackgroundControls = [
5085
5090
  { id: "field", label: "\u041F\u043E\u043B\u0435", controls: [
@@ -5152,6 +5157,7 @@ var QuantumBackground = /* @__PURE__ */ forwardRef3(function QuantumBackground2(
5152
5157
  fullscreen = false,
5153
5158
  spectrum,
5154
5159
  spectrumRange,
5160
+ threeUrl,
5155
5161
  children,
5156
5162
  onTransportChange,
5157
5163
  onWaveformChange,
@@ -5268,7 +5274,7 @@ var QuantumBackground = /* @__PURE__ */ forwardRef3(function QuantumBackground2(
5268
5274
  {
5269
5275
  ref: frame,
5270
5276
  title: "Quantum Background visualizer",
5271
- srcDoc: reference_default,
5277
+ srcDoc: sceneDocument(threeUrl ?? quantumBackgroundThreeUrl),
5272
5278
  allow: "microphone",
5273
5279
  "data-particle-budget": normalizedBudget,
5274
5280
  "data-particle-count": normalizedCount
@@ -7089,6 +7095,7 @@ export {
7089
7095
  quantumBackgroundFields,
7090
7096
  quantumBackgroundFullscreenFieldSize,
7091
7097
  quantumBackgroundMaxParticles,
7098
+ quantumBackgroundThreeUrl,
7092
7099
  quantumFieldAudio,
7093
7100
  quantumFieldExperienceControls,
7094
7101
  quantumFieldModes,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neo-ui-kit/react",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "NEO UI — React component library",
5
5
  "type": "module",
6
6
  "license": "MIT",