Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Maximalist 3D Audio Visualization</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/controls/OrbitControls.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/postprocessing/EffectComposer.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/postprocessing/RenderPass.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/postprocessing/ShaderPass.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/postprocessing/UnrealBloomPass.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/shaders/CopyShader.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/shaders/LuminosityHighPassShader.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/postprocessing/SSAOPass.js"></script> | |
| <style> | |
| body { | |
| margin: 0; | |
| overflow: hidden; | |
| background: #000; | |
| color: white; | |
| font-family: 'Arial', sans-serif; | |
| } | |
| #canvas-container { | |
| position: absolute; | |
| width: 100%; | |
| height: 100%; | |
| } | |
| #ui { | |
| position: absolute; | |
| bottom: 20px; | |
| left: 20px; | |
| z-index: 100; | |
| background: rgba(0, 0, 0, 0.5); | |
| padding: 15px; | |
| border-radius: 10px; | |
| backdrop-filter: blur(5px); | |
| } | |
| #audio-controls { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| #file-input { | |
| display: none; | |
| } | |
| .control-btn { | |
| background: rgba(255, 255, 255, 0.1); | |
| border: 1px solid rgba(255, 255, 255, 0.2); | |
| color: white; | |
| padding: 8px 15px; | |
| border-radius: 5px; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| } | |
| .control-btn:hover { | |
| background: rgba(255, 255, 255, 0.2); | |
| } | |
| .slider-container { | |
| margin-top: 10px; | |
| } | |
| .slider-container label { | |
| display: block; | |
| margin-bottom: 5px; | |
| font-size: 12px; | |
| } | |
| .slider { | |
| width: 100%; | |
| } | |
| #loading { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: #000; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| z-index: 1000; | |
| flex-direction: column; | |
| } | |
| .spinner { | |
| width: 50px; | |
| height: 50px; | |
| border: 5px solid rgba(255, 255, 255, 0.1); | |
| border-radius: 50%; | |
| border-top-color: #4f46e5; | |
| animation: spin 1s ease-in-out infinite; | |
| margin-bottom: 20px; | |
| } | |
| @keyframes spin { | |
| to { transform: rotate(360deg); } | |
| } | |
| #visual-presets { | |
| margin-top: 15px; | |
| } | |
| .preset-btn { | |
| background: rgba(255, 255, 255, 0.05); | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| color: white; | |
| padding: 5px 10px; | |
| border-radius: 3px; | |
| cursor: pointer; | |
| margin-right: 5px; | |
| font-size: 12px; | |
| transition: all 0.2s; | |
| } | |
| .preset-btn:hover { | |
| background: rgba(255, 255, 255, 0.2); | |
| } | |
| .preset-btn.active { | |
| background: #4f46e5; | |
| border-color: #4f46e5; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="loading"> | |
| <div class="spinner"></div> | |
| <div>Loading 3D visualization engine...</div> | |
| </div> | |
| <div id="canvas-container"></div> | |
| <div id="ui"> | |
| <div id="audio-controls"> | |
| <button id="play-btn" class="control-btn">Play</button> | |
| <button id="pause-btn" class="control-btn">Pause</button> | |
| <button id="file-btn" class="control-btn">Load Audio</button> | |
| <input type="file" id="file-input" accept="audio/*"> | |
| <button id="mic-btn" class="control-btn">Use Microphone</button> | |
| </div> | |
| <div class="slider-container"> | |
| <label for="volume-slider">Volume</label> | |
| <input type="range" id="volume-slider" class="slider" min="0" max="1" step="0.01" value="0.7"> | |
| </div> | |
| <div class="slider-container"> | |
| <label for="bass-slider">Bass Intensity</label> | |
| <input type="range" id="bass-slider" class="slider" min="0" max="2" step="0.1" value="1"> | |
| </div> | |
| <div class="slider-container"> | |
| <label for="mid-slider">Mid Intensity</label> | |
| <input type="range" id="mid-slider" class="slider" min="0" max="2" step="0.1" value="1"> | |
| </div> | |
| <div class="slider-container"> | |
| <label for="high-slider">High Intensity</label> | |
| <input type="range" id="high-slider" class="slider" min="0" max="2" step="0.1" value="1"> | |
| </div> | |
| <div id="visual-presets"> | |
| <div style="margin-bottom: 5px; font-size: 12px;">Visual Presets:</div> | |
| <button class="preset-btn active" data-preset="default">Neon Geometry</button> | |
| <button class="preset-btn" data-preset="crystalline">Crystalline</button> | |
| <button class="preset-btn" data-preset="organic">Organic</button> | |
| <button class="preset-btn" data-preset="wireframe">Wireframe</button> | |
| </div> | |
| </div> | |
| <script> | |
| // Wait for everything to load | |
| document.addEventListener('DOMContentLoaded', () => { | |
| setTimeout(() => { | |
| document.getElementById('loading').style.display = 'none'; | |
| init(); | |
| }, 1500); | |
| }); | |
| let scene, camera, renderer, composer, bloomPass, audioContext, analyser, dataArray; | |
| let bassGroup, midGroup, highGroup, particleSystem, wireframeStructures = []; | |
| let audioSource, isPlaying = false, currentPreset = 'default'; | |
| let cameraTargets = []; | |
| let currentCameraTarget = 0; | |
| let lastCameraChange = 0; | |
| let cameraControls; | |
| let clock = new THREE.Clock(); | |
| // Frequency ranges | |
| const BASS_RANGE = [20, 140]; | |
| const MID_RANGE = [140, 2000]; | |
| const HIGH_RANGE = [2000, 20000]; | |
| // Color palettes for different presets | |
| const colorPalettes = { | |
| default: { | |
| bass: [0x4f46e5, 0x8b5cf6, 0xa78bfa], | |
| mid: [0xec4899, 0xf472b6, 0xf9a8d4], | |
| high: [0x10b981, 0x34d399, 0x6ee7b7], | |
| particles: [0xffffff, 0xfacc15, 0xf472b6], | |
| wireframe: 0x4f46e5, | |
| ambient: 0x111827 | |
| }, | |
| crystalline: { | |
| bass: [0x3b82f6, 0x60a5fa, 0x93c5fd], | |
| mid: [0x06b6d4, 0x22d3ee, 0x67e8f9], | |
| high: [0xa855f7, 0xc084fc, 0xd8b4fe], | |
| particles: [0xffffff, 0xe0e7ff, 0xbfdbfe], | |
| wireframe: 0x3b82f6, | |
| ambient: 0x0c1a32 | |
| }, | |
| organic: { | |
| bass: [0x10b981, 0x34d399, 0x6ee7b7], | |
| mid: [0xf59e0b, 0xfbbf24, 0xfcd34d], | |
| high: [0xef4444, 0xf87171, 0xfca5a5], | |
| particles: [0xffffff, 0xfef3c7, 0xfecaca], | |
| wireframe: 0x10b981, | |
| ambient: 0x1c1917 | |
| }, | |
| wireframe: { | |
| bass: [0xffffff, 0xe5e7eb, 0xd1d5db], | |
| mid: [0xffffff, 0xe5e7eb, 0xd1d5db], | |
| high: [0xffffff, 0xe5e7eb, 0xd1d5db], | |
| particles: [0xffffff, 0xe5e7eb, 0xd1d5db], | |
| wireframe: 0xffffff, | |
| ambient: 0x000000 | |
| } | |
| }; | |
| function init() { | |
| // Create scene | |
| scene = new THREE.Scene(); | |
| scene.background = new THREE.Color(0x000000); | |
| scene.fog = new THREE.FogExp2(0x000000, 0.002); | |
| // Create camera | |
| camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); | |
| camera.position.set(0, 0, 30); | |
| // Create renderer with antialiasing | |
| renderer = new THREE.WebGLRenderer({ antialias: true }); | |
| renderer.setPixelRatio(window.devicePixelRatio); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| renderer.shadowMap.enabled = true; | |
| renderer.shadowMap.type = THREE.PCFSoftShadowMap; | |
| renderer.toneMapping = THREE.ACESFilmicToneMapping; | |
| renderer.toneMappingExposure = 1.5; | |
| document.getElementById('canvas-container').appendChild(renderer.domElement); | |
| // Post-processing setup | |
| composer = new THREE.EffectComposer(renderer); | |
| const renderPass = new THREE.RenderPass(scene, camera); | |
| composer.addPass(renderPass); | |
| bloomPass = new THREE.UnrealBloomPass( | |
| new THREE.Vector2(window.innerWidth, window.innerHeight), | |
| 1.5, 0.4, 0.85 | |
| ); | |
| composer.addPass(bloomPass); | |
| // Camera controls | |
| cameraControls = new THREE.OrbitControls(camera, renderer.domElement); | |
| cameraControls.enableDamping = true; | |
| cameraControls.dampingFactor = 0.05; | |
| cameraControls.screenSpacePanning = false; | |
| cameraControls.maxPolarAngle = Math.PI; | |
| cameraControls.minDistance = 10; | |
| cameraControls.maxDistance = 100; | |
| // Setup lights | |
| setupLights(); | |
| // Create audio visualizer elements | |
| createVisualizerElements(); | |
| // Setup camera targets | |
| setupCameraTargets(); | |
| // Setup event listeners | |
| setupEventListeners(); | |
| // Start animation loop | |
| animate(); | |
| // Initialize Web Audio API | |
| initAudio(); | |
| } | |
| function setupLights() { | |
| // Ambient light | |
| const ambientLight = new THREE.AmbientLight(colorPalettes[currentPreset].ambient, 0.2); | |
| scene.add(ambientLight); | |
| // Directional light | |
| const directionalLight = new THREE.DirectionalLight(0xffffff, 1); | |
| directionalLight.position.set(1, 1, 1); | |
| directionalLight.castShadow = true; | |
| directionalLight.shadow.mapSize.width = 2048; | |
| directionalLight.shadow.mapSize.height = 2048; | |
| directionalLight.shadow.camera.near = 0.5; | |
| directionalLight.shadow.camera.far = 500; | |
| directionalLight.shadow.camera.left = -50; | |
| directionalLight.shadow.camera.right = 50; | |
| directionalLight.shadow.camera.top = 50; | |
| directionalLight.shadow.camera.bottom = -50; | |
| scene.add(directionalLight); | |
| // Point lights | |
| const pointLight1 = new THREE.PointLight(0xff00ff, 1, 50); | |
| pointLight1.position.set(20, 20, 20); | |
| scene.add(pointLight1); | |
| const pointLight2 = new THREE.PointLight(0x00ffff, 1, 50); | |
| pointLight2.position.set(-20, -20, 20); | |
| scene.add(pointLight2); | |
| // Hemisphere light | |
| const hemisphereLight = new THREE.HemisphereLight(0xffffbb, 0x080820, 0.5); | |
| scene.add(hemisphereLight); | |
| } | |
| function createVisualizerElements() { | |
| // Create groups for different frequency ranges | |
| bassGroup = new THREE.Group(); | |
| midGroup = new THREE.Group(); | |
| highGroup = new THREE.Group(); | |
| scene.add(bassGroup, midGroup, highGroup); | |
| // Create bass elements (large geometric forms) | |
| createBassElements(); | |
| // Create mid elements (medium geometric forms) | |
| createMidElements(); | |
| // Create high elements (small geometric forms and particles) | |
| createHighElements(); | |
| // Create particle system | |
| createParticleSystem(); | |
| // Create wireframe structures | |
| createWireframeStructures(); | |
| // Create floating crystalline structures | |
| createCrystallineStructures(); | |
| // Create organic morphing forms | |
| createOrganicForms(); | |
| } | |
| function createBassElements() { | |
| // Large central dodecahedron | |
| const geometry = new THREE.DodecahedronGeometry(5, 0); | |
| const material = new THREE.MeshPhysicalMaterial({ | |
| color: colorPalettes[currentPreset].bass[0], | |
| metalness: 0.8, | |
| roughness: 0.2, | |
| clearcoat: 1, | |
| clearcoatRoughness: 0.1, | |
| transmission: 0.5, | |
| ior: 1.5, | |
| envMapIntensity: 1, | |
| emissive: colorPalettes[currentPreset].bass[0], | |
| emissiveIntensity: 0.2 | |
| }); | |
| const mesh = new THREE.Mesh(geometry, material); | |
| mesh.castShadow = true; | |
| mesh.receiveShadow = true; | |
| bassGroup.add(mesh); | |
| // Surrounding icosahedrons | |
| for (let i = 0; i < 6; i++) { | |
| const angle = (i / 6) * Math.PI * 2; | |
| const radius = 8 + Math.random() * 2; | |
| const x = Math.cos(angle) * radius; | |
| const y = Math.sin(angle) * radius; | |
| const geo = new THREE.IcosahedronGeometry(2 + Math.random(), 1); | |
| const mat = new THREE.MeshPhysicalMaterial({ | |
| color: colorPalettes[currentPreset].bass[1 + Math.floor(Math.random() * 2)], | |
| metalness: 0.7, | |
| roughness: 0.3, | |
| clearcoat: 0.5, | |
| emissive: colorPalettes[currentPreset].bass[1 + Math.floor(Math.random() * 2)], | |
| emissiveIntensity: 0.1 | |
| }); | |
| const icosa = new THREE.Mesh(geo, mat); | |
| icosa.position.set(x, y, 0); | |
| icosa.castShadow = true; | |
| icosa.receiveShadow = true; | |
| bassGroup.add(icosa); | |
| } | |
| } | |
| function createMidElements() { | |
| // Torus knots | |
| for (let i = 0; i < 12; i++) { | |
| const angle = (i / 12) * Math.PI * 2; | |
| const radius = 12 + Math.random() * 3; | |
| const x = Math.cos(angle) * radius; | |
| const y = Math.sin(angle) * radius; | |
| const z = (Math.random() - 0.5) * 10; | |
| const geo = new THREE.TorusKnotGeometry( | |
| 1 + Math.random() * 0.5, | |
| 0.3 + Math.random() * 0.2, | |
| 100, 16 | |
| ); | |
| const mat = new THREE.MeshPhysicalMaterial({ | |
| color: colorPalettes[currentPreset].mid[i % colorPalettes[currentPreset].mid.length], | |
| metalness: 0.6, | |
| roughness: 0.4, | |
| clearcoat: 0.7, | |
| emissive: colorPalettes[currentPreset].mid[i % colorPalettes[currentPreset].mid.length], | |
| emissiveIntensity: 0.2 | |
| }); | |
| const torus = new THREE.Mesh(geo, mat); | |
| torus.position.set(x, y, z); | |
| torus.castShadow = true; | |
| torus.receiveShadow = true; | |
| midGroup.add(torus); | |
| } | |
| } | |
| function createHighElements() { | |
| // Small spheres | |
| for (let i = 0; i < 24; i++) { | |
| const angle = (i / 24) * Math.PI * 2; | |
| const radius = 15 + Math.random() * 5; | |
| const x = Math.cos(angle) * radius; | |
| const y = Math.sin(angle) * radius; | |
| const z = (Math.random() - 0.5) * 15; | |
| const geo = new THREE.SphereGeometry(0.5 + Math.random() * 0.3, 16, 16); | |
| const mat = new THREE.MeshPhysicalMaterial({ | |
| color: colorPalettes[currentPreset].high[i % colorPalettes[currentPreset].high.length], | |
| metalness: 0.5, | |
| roughness: 0.5, | |
| clearcoat: 0.8, | |
| emissive: colorPalettes[currentPreset].high[i % colorPalettes[currentPreset].high.length], | |
| emissiveIntensity: 0.3 | |
| }); | |
| const sphere = new THREE.Mesh(geo, mat); | |
| sphere.position.set(x, y, z); | |
| sphere.castShadow = true; | |
| sphere.receiveShadow = true; | |
| highGroup.add(sphere); | |
| } | |
| } | |
| function createParticleSystem() { | |
| const particleCount = 2000; | |
| const particles = new THREE.BufferGeometry(); | |
| const positions = new Float32Array(particleCount * 3); | |
| const colors = new Float32Array(particleCount * 3); | |
| const sizes = new Float32Array(particleCount); | |
| for (let i = 0; i < particleCount; i++) { | |
| // Positions | |
| const radius = 5 + Math.random() * 20; | |
| const theta = Math.random() * Math.PI * 2; | |
| const phi = Math.acos(2 * Math.random() - 1); | |
| positions[i * 3] = radius * Math.sin(phi) * Math.cos(theta); | |
| positions[i * 3 + 1] = radius * Math.sin(phi) * Math.sin(theta); | |
| positions[i * 3 + 2] = radius * Math.cos(phi); | |
| // Colors | |
| const colorIndex = Math.floor(Math.random() * colorPalettes[currentPreset].particles.length); | |
| const color = new THREE.Color(colorPalettes[currentPreset].particles[colorIndex]); | |
| colors[i * 3] = color.r; | |
| colors[i * 3 + 1] = color.g; | |
| colors[i * 3 + 2] = color.b; | |
| // Sizes | |
| sizes[i] = 0.1 + Math.random() * 0.5; | |
| } | |
| particles.setAttribute('position', new THREE.BufferAttribute(positions, 3)); | |
| particles.setAttribute('color', new THREE.BufferAttribute(colors, 3)); | |
| particles.setAttribute('size', new THREE.BufferAttribute(sizes, 1)); | |
| const particleMaterial = new THREE.PointsMaterial({ | |
| size: 0.2, | |
| vertexColors: true, | |
| transparent: true, | |
| opacity: 0.8, | |
| blending: THREE.AdditiveBlending, | |
| sizeAttenuation: true | |
| }); | |
| particleSystem = new THREE.Points(particles, particleMaterial); | |
| scene.add(particleSystem); | |
| } | |
| function createWireframeStructures() { | |
| // Create several complex wireframe structures | |
| for (let i = 0; i < 5; i++) { | |
| const group = new THREE.Group(); | |
| // Create a base shape | |
| let geometry; | |
| switch (i % 3) { | |
| case 0: | |
| geometry = new THREE.OctahedronGeometry(3 + Math.random() * 2, 1); | |
| break; | |
| case 1: | |
| geometry = new THREE.TorusKnotGeometry(2 + Math.random(), 0.5, 100, 16); | |
| break; | |
| case 2: | |
| geometry = new THREE.IcosahedronGeometry(3 + Math.random(), 1); | |
| break; | |
| } | |
| // Convert to wireframe | |
| const wireframe = new THREE.LineSegments( | |
| new THREE.WireframeGeometry(geometry), | |
| new THREE.LineBasicMaterial({ | |
| color: colorPalettes[currentPreset].wireframe, | |
| transparent: true, | |
| opacity: 0.6, | |
| linewidth: 2 | |
| }) | |
| ); | |
| // Position randomly | |
| wireframe.position.x = (Math.random() - 0.5) * 30; | |
| wireframe.position.y = (Math.random() - 0.5) * 30; | |
| wireframe.position.z = (Math.random() - 0.5) * 30; | |
| // Random rotation | |
| wireframe.rotation.x = Math.random() * Math.PI; | |
| wireframe.rotation.y = Math.random() * Math.PI; | |
| group.add(wireframe); | |
| scene.add(group); | |
| wireframeStructures.push(group); | |
| } | |
| } | |
| function createCrystallineStructures() { | |
| // Create several crystalline structures | |
| for (let i = 0; i < 8; i++) { | |
| const group = new THREE.Group(); | |
| // Create a cluster of crystals | |
| const crystalCount = 3 + Math.floor(Math.random() * 5); | |
| for (let j = 0; j < crystalCount; j++) { | |
| const height = 1 + Math.random() * 2; | |
| const radius = 0.3 + Math.random() * 0.5; | |
| const sides = 4 + Math.floor(Math.random() * 3) * 2; // 4, 6 or 8 sides | |
| const geometry = new THREE.CylinderGeometry( | |
| 0, radius, height, sides, 1, true | |
| ); | |
| const material = new THREE.MeshPhysicalMaterial({ | |
| color: 0xffffff, | |
| metalness: 0.1, | |
| roughness: 0.1, | |
| transmission: 0.9, | |
| ior: 1.5, | |
| thickness: 0.5, | |
| envMapIntensity: 1, | |
| emissive: colorPalettes[currentPreset].high[i % colorPalettes[currentPreset].high.length], | |
| emissiveIntensity: 0.3 | |
| }); | |
| const crystal = new THREE.Mesh(geometry, material); | |
| // Position within cluster | |
| crystal.position.x = (Math.random() - 0.5) * 3; | |
| crystal.position.y = (Math.random() - 0.5) * 3; | |
| crystal.position.z = (Math.random() - 0.5) * 3; | |
| // Rotate randomly | |
| crystal.rotation.x = Math.random() * Math.PI; | |
| crystal.rotation.y = Math.random() * Math.PI; | |
| crystal.rotation.z = Math.random() * Math.PI; | |
| group.add(crystal); | |
| } | |
| // Position cluster in scene | |
| group.position.x = (Math.random() - 0.5) * 40; | |
| group.position.y = (Math.random() - 0.5) * 40; | |
| group.position.z = (Math.random() - 0.5) * 40; | |
| scene.add(group); | |
| wireframeStructures.push(group); // Reuse same array for simplicity | |
| } | |
| } | |
| function createOrganicForms() { | |
| // Create several organic-looking forms | |
| for (let i = 0; i < 4; i++) { | |
| const group = new THREE.Group(); | |
| // Create a blob-like shape | |
| const geometry = new THREE.SphereGeometry( | |
| 1.5 + Math.random(), | |
| 32, 32 | |
| ); | |
| // Displace vertices to create organic shape | |
| const positionAttribute = geometry.getAttribute('position'); | |
| const vertex = new THREE.Vector3(); | |
| for (let j = 0; j < positionAttribute.count; j++) { | |
| vertex.fromBufferAttribute(positionAttribute, j); | |
| vertex.normalize(); | |
| const radius = 1 + Math.random() * 0.5; | |
| vertex.multiplyScalar(radius); | |
| positionAttribute.setXYZ(j, vertex.x, vertex.y, vertex.z); | |
| } | |
| geometry.computeVertexNormals(); | |
| const material = new THREE.MeshPhysicalMaterial({ | |
| color: colorPalettes[currentPreset].mid[i % colorPalettes[currentPreset].mid.length], | |
| metalness: 0.3, | |
| roughness: 0.7, | |
| clearcoat: 0.5, | |
| transmission: 0.3, | |
| emissive: colorPalettes[currentPreset].mid[i % colorPalettes[currentPreset].mid.length], | |
| emissiveIntensity: 0.2 | |
| }); | |
| const blob = new THREE.Mesh(geometry, material); | |
| // Position in scene | |
| blob.position.x = (Math.random() - 0.5) * 30; | |
| blob.position.y = (Math.random() - 0.5) * 30; | |
| blob.position.z = (Math.random() - 0.5) * 30; | |
| group.add(blob); | |
| scene.add(group); | |
| wireframeStructures.push(group); // Reuse same array for simplicity | |
| } | |
| } | |
| function setupCameraTargets() { | |
| // Create several interesting camera positions and look-at points | |
| cameraTargets = [ | |
| { position: new THREE.Vector3(0, 0, 30), lookAt: new THREE.Vector3(0, 0, 0) }, | |
| { position: new THREE.Vector3(20, 10, 20), lookAt: new THREE.Vector3(0, 0, 0) }, | |
| { position: new THREE.Vector3(-15, 15, 25), lookAt: new THREE.Vector3(0, 5, 0) }, | |
| { position: new THREE.Vector3(0, 25, 15), lookAt: new THREE.Vector3(0, 0, 0) }, | |
| { position: new THREE.Vector3(30, 0, 0), lookAt: new THREE.Vector3(0, 0, 0) }, | |
| { position: new THREE.Vector3(-20, -10, 20), lookAt: new THREE.Vector3(0, 0, 0) } | |
| ]; | |
| } | |
| function setupEventListeners() { | |
| // Window resize | |
| window.addEventListener('resize', onWindowResize); | |
| // Audio controls | |
| document.getElementById('play-btn').addEventListener('click', playAudio); | |
| document.getElementById('pause-btn').addEventListener('click', pauseAudio); | |
| document.getElementById('file-btn').addEventListener('click', () => document.getElementById('file-input').click()); | |
| document.getElementById('mic-btn').addEventListener('click', useMicrophone); | |
| document.getElementById('file-input').addEventListener('change', handleFileSelect); | |
| // Sliders | |
| document.getElementById('volume-slider').addEventListener('input', (e) => { | |
| if (audioSource) audioSource.gain.value = e.target.value; | |
| }); | |
| // Preset buttons | |
| document.querySelectorAll('.preset-btn').forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| currentPreset = btn.dataset.preset; | |
| document.querySelectorAll('.preset-btn').forEach(b => b.classList.remove('active')); | |
| btn.classList.add('active'); | |
| updateMaterials(); | |
| }); | |
| }); | |
| } | |
| function initAudio() { | |
| audioContext = new (window.AudioContext || window.webkitAudioContext)(); | |
| analyser = audioContext.createAnalyser(); | |
| analyser.fftSize = 2048; | |
| dataArray = new Uint8Array(analyser.frequencyBinCount); | |
| } | |
| function playAudio() { | |
| if (!isPlaying && audioSource) { | |
| audioSource.connect(analyser); | |
| analyser.connect(audioContext.destination); | |
| isPlaying = true; | |
| } | |
| } | |
| function pauseAudio() { | |
| if (isPlaying && audioSource) { | |
| audioSource.disconnect(); | |
| isPlaying = false; | |
| } | |
| } | |
| function useMicrophone() { | |
| navigator.mediaDevices.getUserMedia({ audio: true, video: false }) | |
| .then(stream => { | |
| if (audioSource) audioSource.disconnect(); | |
| audioSource = audioContext.createMediaStreamSource(stream); | |
| audioSource.connect(analyser); | |
| analyser.connect(audioContext.destination); | |
| // Create gain node for volume control | |
| const gainNode = audioContext.createGain(); | |
| audioSource.disconnect(); | |
| audioSource.connect(gainNode); | |
| gainNode.connect(analyser); | |
| audioSource = gainNode; | |
| // Set initial volume | |
| gainNode.gain.value = document.getElementById('volume-slider').value; | |
| isPlaying = true; | |
| }) | |
| .catch(err => { | |
| console.error('Error accessing microphone:', err); | |
| alert('Could not access microphone. Please check permissions.'); | |
| }); | |
| } | |
| function handleFileSelect(event) { | |
| const file = event.target.files[0]; | |
| if (!file) return; | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| audioContext.decodeAudioData(e.target.result) | |
| .then(buffer => { | |
| if (audioSource) audioSource.disconnect(); | |
| audioSource = audioContext.createBufferSource(); | |
| audioSource.buffer = buffer; | |
| // Create gain node for volume control | |
| const gainNode = audioContext.createGain(); | |
| audioSource.connect(gainNode); | |
| gainNode.connect(analyser); | |
| audioSource = gainNode; | |
| // Set initial volume | |
| gainNode.gain.value = document.getElementById('volume-slider').value; | |
| audioSource.loop = true; | |
| audioSource.start(); | |
| isPlaying = true; | |
| }) | |
| .catch(err => { | |
| console.error('Error decoding audio file:', err); | |
| alert('Error loading audio file. Please try a different file.'); | |
| }); | |
| }; | |
| reader.readAsArrayBuffer(file); | |
| } | |
| function updateMaterials() { | |
| // Update all materials based on current preset | |
| scene.traverse(obj => { | |
| if (obj.isMesh) { | |
| if (obj.material instanceof THREE.MeshPhysicalMaterial) { | |
| // Determine which color palette to use based on which group the object is in | |
| let palette; | |
| if (bassGroup.children.includes(obj)) palette = colorPalettes[currentPreset].bass; | |
| else if (midGroup.children.includes(obj)) palette = colorPalettes[currentPreset].mid; | |
| else if (highGroup.children.includes(obj)) palette = colorPalettes[currentPreset].high; | |
| else palette = colorPalettes[currentPreset].mid; // Default | |
| // Select a color from the palette | |
| const colorIndex = Math.floor(Math.random() * palette.length); | |
| const color = palette[colorIndex]; | |
| // Update material properties | |
| obj.material.color.setHex(color); | |
| obj.material.emissive.setHex(color); | |
| } | |
| } else if (obj.isLine) { | |
| obj.material.color.setHex(colorPalettes[currentPreset].wireframe); | |
| } | |
| }); | |
| // Update particle colors | |
| if (particleSystem) { | |
| const colors = particleSystem.geometry.attributes.color; | |
| for (let i = 0; i < colors.count; i++) { | |
| const colorIndex = Math.floor(Math.random() * colorPalettes[currentPreset].particles.length); | |
| const color = new THREE.Color(colorPalettes[currentPreset].particles[colorIndex]); | |
| colors.setXYZ(i, color.r, color.g, color.b); | |
| } | |
| colors.needsUpdate = true; | |
| } | |
| // Update ambient light | |
| scene.traverse(obj => { | |
| if (obj.isAmbientLight) { | |
| obj.color.setHex(colorPalettes[currentPreset].ambient); | |
| } | |
| }); | |
| } | |
| function onWindowResize() { | |
| camera.aspect = window.innerWidth / window.innerHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| composer.setSize(window.innerWidth, window.innerHeight); | |
| } | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| const delta = clock.getDelta(); | |
| const time = clock.getElapsedTime(); | |
| // Update audio analyzer data if playing | |
| if (isPlaying && analyser) { | |
| analyser.getByteFrequencyData(dataArray); | |
| // Calculate frequency ranges | |
| const bassData = getFrequencyRangeValue(dataArray, BASS_RANGE); | |
| const midData = getFrequencyRangeValue(dataArray, MID_RANGE); | |
| const highData = getFrequencyRangeValue(dataArray, HIGH_RANGE); | |
| const overallVolume = getOverallVolume(dataArray); | |
| // Apply intensity multipliers from sliders | |
| const bassIntensity = document.getElementById('bass-slider').value; | |
| const midIntensity = document.getElementById('mid-slider').value; | |
| const highIntensity = document.getElementById('high-slider').value; | |
| // Update visual elements based on audio data | |
| updateBassElements(bassData * bassIntensity, time); | |
| updateMidElements(midData * midIntensity, time); | |
| updateHighElements(highData * highIntensity, time); | |
| updateParticleSystem(highData * highIntensity, time); | |
| updateWireframeStructures(overallVolume, time); | |
| // Update bloom effect based on overall volume | |
| bloomPass.strength = 1 + overallVolume * 2; | |
| } | |
| // Rotate groups slowly | |
| bassGroup.rotation.y += 0.001; | |
| midGroup.rotation.y -= 0.0005; | |
| highGroup.rotation.x += 0.0003; | |
| // Move camera between targets periodically | |
| if (time - lastCameraChange > 8) { | |
| lastCameraChange = time; | |
| currentCameraTarget = (currentCameraTarget + 1) % cameraTargets.length; | |
| } | |
| // Smooth camera transition | |
| const target = cameraTargets[currentCameraTarget]; | |
| camera.position.lerp(target.position, 0.01); | |
| cameraControls.target.lerp(target.lookAt, 0.01); | |
| // Update camera controls | |
| cameraControls.update(); | |
| // Render scene with post-processing | |
| composer.render(); | |
| } | |
| function getFrequencyRangeValue(dataArray, [lowFreq, highFreq]) { | |
| const sampleRate = audioContext.sampleRate; | |
| const binCount = analyser.frequencyBinCount; | |
| const lowIndex = Math.round(lowFreq / (sampleRate / 2) * binCount); | |
| const highIndex = Math.round(highFreq / (sampleRate / 2) * binCount); | |
| let total = 0; | |
| let count = 0; | |
| for (let i = lowIndex; i <= highIndex; i++) { | |
| total += dataArray[i]; | |
| count++; | |
| } | |
| return count > 0 ? total / count / 255 : 0; | |
| } | |
| function getOverallVolume(dataArray) { | |
| let total = 0; | |
| for (let i = 0; i < dataArray.length; i++) { | |
| total += dataArray[i]; | |
| } | |
| return total / dataArray.length / 255; | |
| } | |
| function updateBassElements(intensity, time) { | |
| bassGroup.children.forEach((child, i) => { | |
| if (i === 0) { | |
| // Main central shape - scale with bass | |
| const scale = 1 + intensity * 0.5; | |
| child.scale.set(scale, scale, scale); | |
| // Pulsing emissive | |
| child.material.emissiveIntensity = 0.2 + intensity * 0.8; | |
| } else { | |
| // Surrounding shapes - move outward with bass | |
| const angle = (i / (bassGroup.children.length - 1)) * Math.PI * 2; | |
| const baseRadius = 8; | |
| const radius = baseRadius + intensity * 5; | |
| child.position.x = Math.cos(angle + time * 0.2) * radius; | |
| child.position.y = Math.sin(angle + time * 0.2) * radius; | |
| // Rotate | |
| child.rotation.x += 0.01; | |
| child.rotation.y += 0.01; | |
| // Scale slightly | |
| const scale = 1 + intensity * 0.3; | |
| child.scale.set(scale, scale, scale); | |
| } | |
| }); | |
| } | |
| function updateMidElements(intensity, time) { | |
| midGroup.children.forEach((child, i) => { | |
| // Move in circular patterns | |
| const angle = (i / midGroup.children.length) * Math.PI * 2; | |
| const baseRadius = 12; | |
| const radius = baseRadius + intensity * 3; | |
| child.position.x = Math.cos(angle + time * 0.3) * radius; | |
| child.position.y = Math.sin(angle + time * 0.3) * radius; | |
| // Rotate | |
| child.rotation.x += 0.02; | |
| child.rotation.z += 0.01; | |
| // Scale | |
| const scale = 1 + intensity * 0.2; | |
| child.scale.set(scale, scale, scale); | |
| // Pulsing emissive | |
| child.material.emissiveIntensity = 0.2 + intensity * 0.6; | |
| }); | |
| } | |
| function updateHighElements(intensity, time) { | |
| highGroup.children.forEach((child, i) => { | |
| // Move in more complex patterns | |
| const angle = (i / highGroup.children.length) * Math.PI * 2; | |
| const baseRadius = 15; | |
| const radius = baseRadius + intensity * 4; | |
| const height = Math.sin(time * 0.5 + i) * 5; | |
| child.position.x = Math.cos(angle + time * 0.4) * radius; | |
| child.position.y = Math.sin(angle + time * 0.4) * radius; | |
| child.position.z = height; | |
| // Scale | |
| const scale = 1 + intensity * 0.4; | |
| child.scale.set(scale, scale, scale); | |
| // Pulsing emissive | |
| child.material.emissiveIntensity = 0.3 + intensity * 0.7; | |
| }); | |
| } | |
| function updateParticleSystem(intensity, time) { | |
| const particles = particleSystem.geometry.attributes.position; | |
| const originalPositions = particleSystem.geometry.attributes.originalPosition || particles; | |
| // Store original positions if not already done | |
| if (!particleSystem.geometry.attributes.originalPosition) { | |
| const origPos = new Float32Array(particles.count * 3); | |
| for (let i = 0; i < particles.count * 3; i++) { | |
| origPos[i] = particles.array[i]; | |
| } | |
| particleSystem.geometry.setAttribute( | |
| 'originalPosition', | |
| new THREE.BufferAttribute(origPos, 3) | |
| ); | |
| } | |
| // Update particle positions based on audio | |
| for (let i = 0; i < particles.count; i++) { | |
| const ix = i * 3; | |
| const iy = i * 3 + 1; | |
| const iz = i * 3 + 2; | |
| // Get original position | |
| const ox = originalPositions.array[ix]; | |
| const oy = originalPositions.array[iy]; | |
| const oz = originalPositions.array[iz]; | |
| // Calculate displacement based on audio | |
| const displacement = intensity * 5; | |
| const noise = Math.sin(time * 2 + i * 0.1) * displacement; | |
| // Apply displacement | |
| particles.array[ix] = ox + noise * 0.3; | |
| particles.array[iy] = oy + noise * 0.3; | |
| particles.array[iz] = oz + noise * 0.3; | |
| } | |
| particles.needsUpdate = true; | |
| // Rotate particle system | |
| particleSystem.rotation.y += 0.001; | |
| } | |
| function updateWireframeStructures(intensity, time) { | |
| wireframeStructures.forEach((group, i) => { | |
| // Float up and down slowly | |
| group.position.y += Math.sin(time * 0.1 + i) * 0.02; | |
| // Rotate | |
| group.rotation.x += 0.005; | |
| group.rotation.y += 0.007; | |
| group.rotation.z += 0.003; | |
| // Scale slightly with audio | |
| const scale = 1 + intensity * 0.5; | |
| group.scale.set(scale, scale, scale); | |
| // Update line opacity based on audio | |
| group.children.forEach(child => { | |
| if (child.isLine) { | |
| child.material.opacity = 0.4 + intensity * 0.6; | |
| } | |
| }); | |
| }); | |
| } | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=Fabriwin/audio-dimension" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |