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> |