import * as THREE from 'https://unpkg.com/three@0.136.0/build/three.module.js'; import { AudioLoader, AudioListener, Audio, AudioAnalyser } from 'https://unpkg.com/three@0.136.0/build/three.module.js'; import * as dat from 'https://cdn.jsdelivr.net/npm/dat.gui/build/dat.gui.module.js'; console.log('Script started'); // Cleanup function function cleanupPreviousElements() { const elementsToRemove = [ '#songSelect', '#playPause', '#volume', 'button', 'select', '.dg.main', 'canvas', '.controls-container', '#volumeControl', '#audioControls', '.audio-control' ]; elementsToRemove.forEach(selector => { document.querySelectorAll(selector).forEach(element => { if (selector !== '#presetContainer') { element.remove(); } }); }); document.querySelectorAll('div').forEach(div => { if ((div.id?.includes('control') || div.className?.includes('control') || div.id?.includes('audio') || div.className?.includes('audio')) && div.id !== 'presetContainer') { div.remove(); } }); } cleanupPreviousElements(); console.log('Cleanup completed'); // Fixed position GUI and presets const guiAndPresetsStyleFix = document.createElement('style'); guiAndPresetsStyleFix.textContent = ` /* Fixed position GUI spheres */ .dg.main { position: absolute !important; top: 10px !important; right: 10px !important; z-index: 1000 !important; } #presetContainer { position: fixed !important; top: 10px !important; left: 10px !important; z-index: 1000 !important; display: flex !important; gap: 10px !important; } #presetContainer input[type="text"] { flex: 1 1 auto; min-width: 150px; padding: 5px; border-radius: 3px; } #presetContainer select, #presetContainer button { flex: 0 0 auto; padding: 5px; border-radius: 3px; } `; document.head.appendChild(guiAndPresetsStyleFix); // Scene setup const scene = new THREE.Scene(); scene.background = new THREE.Color(0x000000); const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); camera.position.z = 2.5; const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); console.log('Scene and renderer initialized'); // Audio setup let audioContext; let analyser; let audioElement; let sourceNode = null; let micStream; let micSource = null; // Audio Controls Container const controls = document.createElement('div'); controls.style.cssText = 'position: absolute; top: 10px; right: 310px; z-index: 1000; background: rgba(0,0,0,0.7); padding: 10px; border-radius: 5px;'; document.body.appendChild(controls); const audioControls = document.createElement('div'); audioControls.style.cssText = 'display: flex; align-items: center; gap: 10px;'; controls.appendChild(audioControls); const songSelect = document.createElement('select'); songSelect.style.cssText = 'padding: 5px; border-radius: 3px; background: #333; color: white; border: 1px solid #666;'; audioControls.appendChild(songSelect); const playPause = document.createElement('button'); playPause.textContent = 'Play'; playPause.style.cssText = 'padding: 5px 15px; border-radius: 3px; background: #444; color: white; border: 1px solid #666;'; audioControls.appendChild(playPause); const volumeControl = document.createElement('input'); volumeControl.type = 'range'; volumeControl.min = 0; volumeControl.max = 1; volumeControl.step = 0.1; volumeControl.value = 0.5; volumeControl.style.width = '100px'; audioControls.appendChild(volumeControl); const timelineControl = document.createElement('input'); timelineControl.type = 'range'; timelineControl.min = 0; timelineControl.max = 100; timelineControl.step = 1; timelineControl.value = 0; timelineControl.style.width = '200px'; timelineControl.style.marginLeft = '10px'; audioControls.appendChild(timelineControl); const inputToggle = document.createElement('button'); inputToggle.textContent = 'Use Mic'; inputToggle.style.cssText = 'padding: 5px 15px; border-radius: 3px; background: #444; color: white; border: 1px solid #666; margin-left: 10px;'; audioControls.appendChild(inputToggle); let usingMic = false; inputToggle.onclick = toggleInput; playPause.onclick = togglePlay; songSelect.onchange = changeSong; console.log('Controls created'); // Noise generator const noise = { p: new Array(256).fill(0).map((_, i) => i), perm: new Array(512), init() { for (let i = this.p.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [this.p[i], this.p[j]] = [this.p[j], this.p[i]]; } for (let i = 0; i < 512; i++) { this.perm[i] = this.p[i & 255]; } }, fade(t) { return t * t * t * (t * (t * 6 - 15) + 10); }, lerp(t, a, b) { return a + t * (b - a); }, grad(hash, x, y, z) { const h = hash & 15; const u = h < 8 ? x : y; const v = h < 4 ? y : h == 12 || h == 14 ? x : z; return ((h & 1) == 0 ? u : -u) + ((h & 2) == 0 ? v : -v); }, noise3D(x, y, z) { const X = Math.floor(x) & 255; const Y = Math.floor(y) & 255; const Z = Math.floor(z) & 255; x -= Math.floor(x); y -= Math.floor(y); z -= Math.floor(z); const u = this.fade(x); const v = this.fade(y); const w = this.fade(z); const A = this.perm[X] + Y; const AA = this.perm[A] + Z; const AB = this.perm[A + 1] + Z; const B = this.perm[X + 1] + Y; const BA = this.perm[B] + Z; const BB = this.perm[B + 1] + Z; return this.lerp(w, this.lerp(v, this.lerp(u, this.grad(this.perm[AA], x, y, z), this.grad(this.perm[BA], x-1, y, z)), this.lerp(u, this.grad(this.perm[AB], x, y-1, z), this.grad(this.perm[BB], x-1, y-1, z))), this.lerp(v, this.lerp(u, this.grad(this.perm[AA+1], x, y, z-1), this.grad(this.perm[BA+1], x-1, y, z-1)), this.lerp(u, this.grad(this.perm[AB+1], x, y-1, z-1), this.grad(this.perm[BB+1], x-1, y-1, z-1)))); } }; noise.init(); console.log('Noise initialized'); // Beat manager const beatManager = { currentWaveRadius: 0, waveStrength: 0, isWaveActive: false, triggerWave(rangeEnergy) { const maxEnergy = 255; const energyExcess = rangeEnergy - 200; this.waveStrength = (energyExcess / (maxEnergy - 200)) * 20.0; this.currentWaveRadius = 0; this.isWaveActive = true; }, update(deltaTime) { if (this.isWaveActive) { this.currentWaveRadius += deltaTime * 1.0; this.waveStrength *= 0.98; if (this.currentWaveRadius > 1.0 || this.waveStrength < 0.1) { this.isWaveActive = false; } } }, getWaveForce(position) { if (!this.isWaveActive) return 0; const distanceFromCenter = position.length(); const distanceFromWave = Math.abs(distanceFromCenter - this.currentWaveRadius); if (distanceFromWave < 0.1) { return this.waveStrength * Math.exp(-distanceFromWave * 10); } return 0; } }; async function toggleInput() { if (!audioContext) { audioContext = new (window.AudioContext || window.webkitAudioContext)(); analyser = audioContext.createAnalyser(); analyser.fftSize = 2048; } await audioContext.resume(); if (usingMic) { usingMic = false; inputToggle.textContent = 'Use Mic'; songSelect.disabled = false; playPause.disabled = false; volumeControl.disabled = false; timelineControl.disabled = false; if (micSource) { micSource.disconnect(); micSource = null; } if (sourceNode) { sourceNode.connect(analyser); } analyser.connect(audioContext.destination); } else { usingMic = true; inputToggle.textContent = 'Use Player'; songSelect.disabled = true; playPause.disabled = true; volumeControl.disabled = true; timelineControl.disabled = true; try { micStream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: false } }); if (sourceNode) { sourceNode.disconnect(); } micSource = audioContext.createMediaStreamSource(micStream); micSource.connect(analyser); analyser.disconnect(audioContext.destination); console.log('Microphone is active'); } catch (error) { console.error("Microphone access failed:", error.name, error.message); usingMic = false; inputToggle.textContent = 'Use Mic'; } } } async function initAudio() { try { if (!audioContext) { audioContext = new (window.AudioContext || window.webkitAudioContext)(); analyser = audioContext.createAnalyser(); analyser.fftSize = 2048; } if (!usingMic) { if (!audioElement) { audioElement = document.createElement('audio'); audioElement.crossOrigin = "anonymous"; audioElement.volume = volumeControl.value; } try { if (!sourceNode) { sourceNode = audioContext.createMediaElementSource(audioElement); sourceNode.connect(analyser); analyser.connect(audioContext.destination); } else { sourceNode.disconnect(); sourceNode.connect(analyser); } } catch (error) { console.error("Failed to connect audio element to analyser:", error.name, error.message); } try { const response = await fetch('Songs/'); const text = await response.text(); const parser = new DOMParser(); const doc = parser.parseFromString(text, 'text/html'); const files = Array.from(doc.querySelectorAll('a')) .map(a => decodeURIComponent(a.href)) .filter(href => href.match(/\.(mp3|wav|ogg)$/i)) .map(href => { const fileName = href.split('/').pop(); return { path: `Songs/${fileName}`, name: fileName.replace(/\.(mp3|wav|ogg)$/i, '') }; }); songSelect.innerHTML = ''; files.forEach(file => { const option = document.createElement('option'); option.value = file.path; option.textContent = file.name; songSelect.appendChild(option); }); if (files.length > 0 && !audioElement.src) { audioElement.src = files[0].path; } } catch (error) { console.error("Failed to load song list:", error); } volumeControl.oninput = e => { if (audioElement) { audioElement.volume = e.target.value; } }; setupTimelineControl(); } console.log('Audio initialized'); } catch (error) { console.error("Audio initialization failed:", error); } } function getAudioData(sphere) { if (!analyser || (!audioContext && !usingMic)) return { average: 0, frequencies: new Float32Array(), peakDetected: false, rangeEnergy: 0 }; try { const frequencies = new Uint8Array(analyser.frequencyBinCount); analyser.getByteFrequencyData(frequencies); const gainMultiplier = sphere.params.gainMultiplier; frequencies.forEach((value, index) => { frequencies[index] = Math.min(value * gainMultiplier, 255); }); const frequencyToIndex = (frequency) => Math.round(frequency / (audioContext.sampleRate / 2) * analyser.frequencyBinCount); const minFreqIndex = frequencyToIndex(sphere.params.minFrequency); const maxFreqIndex = frequencyToIndex(sphere.params.maxFrequency); const frequencyRange = frequencies.slice(minFreqIndex, maxFreqIndex + 1); const rangeEnergy = frequencyRange.reduce((a, b) => a + b, 0) / frequencyRange.length; const minFreqBeatIndex = frequencyToIndex(sphere.params.minFrequencyBeat); // Nové pásmo pro beaty const maxFreqBeatIndex = frequencyToIndex(sphere.params.maxFrequencyBeat); const frequencyRangeBeat = frequencies.slice(minFreqBeatIndex, maxFreqBeatIndex + 1); const rangeEnergyBeat = frequencyRangeBeat.reduce((a, b) => a + b, 0) / frequencyRangeBeat.length; sphere.peakDetection.energyHistory.push(rangeEnergy); if (sphere.peakDetection.energyHistory.length > sphere.peakDetection.historyLength) { sphere.peakDetection.energyHistory.shift(); } const averageEnergy = sphere.peakDetection.energyHistory.reduce((a, b) => a + b, 0) / sphere.peakDetection.energyHistory.length; const now = performance.now(); const peakDetected = rangeEnergy > averageEnergy * sphere.params.peakSensitivity && now - sphere.peakDetection.lastPeakTime > sphere.peakDetection.minTimeBetweenPeaks; if (peakDetected) { sphere.peakDetection.lastPeakTime = now; console.log(`Sphere ${sphere.index + 1} PEAK DETECTED! Energy: ${rangeEnergy}, Average: ${averageEnergy}`); } return { average: rangeEnergy / 255, frequencies, peakDetected, rangeEnergy: rangeEnergy, rangeEnergyBeat: rangeEnergyBeat }; } catch (error) { console.error("Audio analysis failed:", error); return { average: 0, frequencies: new Float32Array(), peakDetected: false, rangeEnergy: 0 }; } } function updateTimeline() { if (audioElement && !audioElement.paused && audioElement.duration) { const percent = (audioElement.currentTime / audioElement.duration) * 100; timelineControl.value = percent; } } function setupTimelineControl() { audioElement.addEventListener('loadedmetadata', () => { timelineControl.value = 0; console.log('Song duration:', audioElement.duration); }); audioElement.addEventListener('timeupdate', updateTimeline); timelineControl.addEventListener('input', (e) => { if (audioElement.duration) { const time = (e.target.value / 100) * audioElement.duration; audioElement.currentTime = time; } }); } function togglePlay() { if (usingMic) return; if (audioContext.state === 'suspended') { audioContext.resume(); } if (!sourceNode && audioElement) { try { sourceNode = audioContext.createMediaElementSource(audioElement); sourceNode.connect(analyser); analyser.connect(audioContext.destination); } catch (error) { console.error("Audio connection failed:", error); return; } } if (audioElement.paused) { audioElement.play() .then(() => playPause.textContent = 'Pause') .catch(error => console.error("Playback failed:", error)); } else { audioElement.pause(); playPause.textContent = 'Play'; } } function changeSong() { if (audioContext.state === 'suspended') { audioContext.resume(); } audioElement.src = songSelect.value; audioElement.load(); if (!sourceNode) { try { sourceNode = audioContext.createMediaElementSource(audioElement); sourceNode.connect(analyser); analyser.connect(audioContext.destination); } catch (error) { console.error("Audio connection failed:", error); return; } } audioElement.play() .then(() => { playPause.textContent = 'Pause'; console.log("Playback started"); }) .catch(error => console.error("Playback failed:", error)); timelineControl.value = 0; } function generateNewNoiseScale(params, lastNoiseScale) { if (!params.dynamicNoiseScale) { return params.noiseScale; } let { minNoiseScale, maxNoiseScale, noiseStep } = params; // --- PŘIDANÁ POJISTKA A DROBNÉ LOGY --- if (minNoiseScale >= maxNoiseScale) { console.warn(`Fixing minNoiseScale (${minNoiseScale}) >= maxNoiseScale (${maxNoiseScale}).`); maxNoiseScale = minNoiseScale + 0.1; // Natvrdo posunout, aby byl rozdíl aspoň 0.1 } let range = maxNoiseScale - minNoiseScale; if (range < 0.1) { console.warn(`Range < 0.1 => Forcing minimal range = 0.1`); range = 0.1; maxNoiseScale = minNoiseScale + range; } if (noiseStep > range) { console.warn(`noiseStep (${noiseStep}) > range (${range}) => Forcing noiseStep = range / 2`); noiseStep = range / 2; } // LastNoiseScale valid range lastNoiseScale = Math.max(minNoiseScale, Math.min(lastNoiseScale, maxNoiseScale)); const stepsUp = Math.floor((maxNoiseScale - lastNoiseScale) / noiseStep); const stepsDown = Math.floor((lastNoiseScale - minNoiseScale) / noiseStep); if (stepsUp === 0 && stepsDown === 0) { return lastNoiseScale; } const direction = Math.random() < 0.5 && stepsDown > 0 ? -1 : 1; const steps = direction === 1 ? Math.floor(Math.random() * (stepsUp + 1)) : Math.floor(Math.random() * (stepsDown + 1)); let newValue = lastNoiseScale + direction * steps * noiseStep; newValue = Math.max(minNoiseScale, Math.min(newValue, maxNoiseScale)); return newValue; } // Reinit particles function reinitializeParticlesForSphere(sphere, sphereParams, sphereGeometry) { console.log(`Reinitializing sphere ${sphere.index + 1} with ${sphereParams.particleCount} particles`); const newPositions = new Float32Array(sphereParams.particleCount * 3); const newColors = new Float32Array(sphereParams.particleCount * 3); const newVelocities = new Float32Array(sphereParams.particleCount * 3); const newBasePositions = new Float32Array(sphereParams.particleCount * 3); const newLifetimes = new Float32Array(sphereParams.particleCount); const newMaxLifetimes = new Float32Array(sphereParams.particleCount); const newBeatEffects = new Float32Array(sphereParams.particleCount); for (let i = 0; i < sphereParams.particleCount; i++) { const i3 = i * 3; const radius = THREE.MathUtils.lerp(0, sphereParams.sphereRadius, sphereParams.innerSphereRadius); const theta = Math.random() * Math.PI * 2; const phi = Math.acos(2 * Math.random() - 1); const r = Math.cbrt(Math.random()) * radius; const x = r * Math.sin(phi) * Math.cos(theta); const y = r * Math.sin(phi) * Math.sin(theta); const z = r * Math.cos(phi); newPositions[i3] = x; newPositions[i3 + 1] = y; newPositions[i3 + 2] = z; newBasePositions[i3] = x; newBasePositions[i3 + 1] = y; newBasePositions[i3 + 2] = z; newVelocities[i3] = 0; newVelocities[i3 + 1] = 0; newVelocities[i3 + 2] = 0; const lt = Math.random() * sphereParams.particleLifetime; newLifetimes[i] = lt; newMaxLifetimes[i] = lt; newBeatEffects[i] = 0; } sphereGeometry.setAttribute('position', new THREE.BufferAttribute(newPositions, 3)); sphereGeometry.setAttribute('color', new THREE.BufferAttribute(newColors, 3)); updateColorsForSphere(sphereParams, sphereGeometry, newColors); return { newPositions, newColors, newVelocities, newBasePositions, newLifetimes, newMaxLifetimes, newBeatEffects }; } function updateColorsForSphere(sphereParams, sphereGeometry, sphereColors) { const color1 = new THREE.Color(sphereParams.colorStart); const color2 = new THREE.Color(sphereParams.colorEnd); for (let i = 0; i < sphereParams.particleCount; i++) { const t = i / sphereParams.particleCount; sphereColors[i * 3] = color1.r * (1 - t) + color2.r * t; sphereColors[i * 3 + 1] = color1.g * (1 - t) + color2.g * t; sphereColors[i * 3 + 2] = color1.b * (1 - t) + color2.b * t; } sphereGeometry.attributes.color.needsUpdate = true; } // Preset management const presets = JSON.parse(localStorage.getItem('presets')) || {}; // Uložené presety const defaultParams = []; // Pro ukládání výchozích hodnot každé sféry // HTML elements presets let presetContainer = document.querySelector('#presetContainer'); let presetInput, saveButton, resetButton, presetSelect, deleteButton, exportButton, importButton; if (!presetContainer) { presetContainer = document.createElement('div'); presetContainer.id = 'presetContainer'; presetContainer.style.cssText = ` position: fixed !important; top: 10px !important; left: 10px !important; z-index: 1000 !important; display: flex !important; gap: 10px !important; `; presetInput = document.createElement('input'); presetInput.type = 'text'; presetInput.placeholder = 'Preset name'; presetInput.style.cssText = 'padding: 5px; border-radius: 3px;'; saveButton = document.createElement('button'); saveButton.textContent = 'Save'; saveButton.style.cssText = 'padding: 5px 10px; border-radius: 3px; background: #444; color: white; border: 1px solid #666;'; resetButton = document.createElement('button'); resetButton.textContent = 'Reset'; resetButton.style.cssText = 'padding: 5px 10px; border-radius: 3px; background: #444; color: white; border: 1px solid #666;'; presetSelect = document.createElement('select'); presetSelect.style.cssText = 'padding: 5px; border-radius: 3px; background: #333; color: white; border: 1px solid #666;'; deleteButton = document.createElement('button'); deleteButton.textContent = 'Delete'; deleteButton.style.cssText = 'padding: 5px 10px; border-radius: 3px; background: #444; color: white; border: 1px solid #666;'; exportButton = document.createElement('button'); exportButton.textContent = 'Export Presets'; exportButton.style.cssText = 'padding: 5px 10px; border-radius: 3px; background: #444; color: white; border: 1px solid #666;'; importButton = document.createElement('button'); importButton.textContent = 'Import Presets'; importButton.style.cssText = 'padding: 5px 10px; border-radius: 3px; background: #444; color: white; border: 1px solid #666;'; presetContainer.appendChild(presetInput); presetContainer.appendChild(saveButton); presetContainer.appendChild(resetButton); presetContainer.appendChild(deleteButton); presetContainer.appendChild(exportButton); presetContainer.appendChild(importButton); presetContainer.appendChild(presetSelect); document.body.appendChild(presetContainer); } // Save presets logic saveButton.onclick = () => { const presetName = presetInput.value.trim(); if (!presetName) return; presets[presetName] = spheres.map(sphere => JSON.parse(JSON.stringify(sphere.params))); localStorage.setItem('presets', JSON.stringify(presets)); updatePresetOptions(); }; // Reset logic resetButton.onclick = () => { spheres.forEach((sphere, index) => { const previousParticleCount = sphere.params.particleCount; // Uložíme původní počet částic Object.assign(sphere.params, defaultParams[index]); sphere.particleSystem.visible = sphere.params.enabled; if (sphere.params.particleCount !== previousParticleCount) { const { newPositions, newColors, newVelocities, newBasePositions, newLifetimes, newMaxLifetimes, newBeatEffects } = reinitializeParticlesForSphere(sphere, sphere.params, sphere.geometry); sphere.positions = newPositions; sphere.colors = newColors; sphere.velocities = newVelocities; sphere.basePositions = newBasePositions; sphere.lifetimes = newLifetimes; sphere.maxLifetimes = newMaxLifetimes; sphere.beatEffects = newBeatEffects; sphere.geometry.attributes.position.needsUpdate = true; sphere.geometry.attributes.color.needsUpdate = true; } const sphereFolder = mainGui.__folders[`Sphere ${index + 1}`]; if (sphereFolder) { sphereFolder.__controllers.forEach(controller => controller.updateDisplay()); } }); console.log("Parameters reset to default values"); mainGui.updateDisplay(); }; // Delete preset logic deleteButton.onclick = () => { const presetName = presetSelect.value; if (!presetName) { console.warn('Žádný preset není vybraný.'); return; } const sure = confirm(`Skutečně smazat preset "${presetName}"?`); if (!sure) return; delete presets[presetName]; localStorage.setItem('presets', JSON.stringify(presets)); updatePresetOptions(); presetSelect.value = ''; presetInput.value = ''; console.log(`Preset "${presetName}" byl smazán.`); }; // Export presets exportButton.onclick = () => { const presetData = JSON.stringify(presets, null, 2); const blob = new Blob([presetData], { type: 'application/json' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = 'particula_presets.json'; link.click(); URL.revokeObjectURL(url); }; // Import presets importButton.onclick = () => { const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.accept = 'application/json'; fileInput.onchange = (event) => { const file = event.target.files[0]; const reader = new FileReader(); reader.onload = (e) => { const importedPresets = JSON.parse(e.target.result); Object.assign(presets, importedPresets); localStorage.setItem('presets', JSON.stringify(presets)); updatePresetOptions(); console.log('Presety byly úspěšně importovány.'); }; reader.readAsText(file); }; fileInput.click(); }; // Upload presets logic presetSelect.onchange = () => { const presetName = presetSelect.value; if (!presetName) return; const preset = presets[presetName]; if (!preset) return; spheres.forEach((sphere, index) => { const previousParticleCount = sphere.params.particleCount; // Původní počet částic Object.assign(sphere.params, preset[index]); if (!('minFrequencyBeat' in sphere.params)) { sphere.params.minFrequencyBeat = sphere.params.minFrequency; } if (!('maxFrequencyBeat' in sphere.params)) { sphere.params.maxFrequencyBeat = sphere.params.maxFrequency; } if (sphere.params.minNoiseScale >= sphere.params.maxNoiseScale) { console.warn(`Preset fix: minNoiseScale (${sphere.params.minNoiseScale}) >= maxNoiseScale (${sphere.params.maxNoiseScale}).`); sphere.params.maxNoiseScale = sphere.params.minNoiseScale + 0.1; } if (sphere.params.particleCount !== previousParticleCount) { const { newPositions, newColors, newVelocities, newBasePositions, newLifetimes, newMaxLifetimes, newBeatEffects } = reinitializeParticlesForSphere(sphere, sphere.params, sphere.geometry); sphere.positions = newPositions; sphere.colors = newColors; sphere.velocities = newVelocities; sphere.basePositions = newBasePositions; sphere.lifetimes = newLifetimes; sphere.maxLifetimes = newMaxLifetimes; sphere.beatEffects = newBeatEffects; sphere.geometry.attributes.position.needsUpdate = true; sphere.geometry.attributes.color.needsUpdate = true; } sphere.particleSystem.visible = sphere.params.enabled; }); mainGui.updateDisplay(); }; // Roll presets function updatePresetOptions() { while (presetSelect.firstChild) { presetSelect.removeChild(presetSelect.firstChild); } const defaultOption = document.createElement('option'); defaultOption.textContent = 'Select preset'; defaultOption.value = ''; presetSelect.appendChild(defaultOption); Object.keys(presets).forEach(name => { const option = document.createElement('option'); option.textContent = name; option.value = name; presetSelect.appendChild(option); }); } // Main GUI panel const mainGui = new dat.GUI(); // FOG PARAMS const fogParams = { enabled: true, color: '#000000', near: 2.7, far: 3.7, }; // After fog update function updateFog() { if (!fogParams.enabled) { scene.fog = null; } else { const color = new THREE.Color(fogParams.color); scene.fog = new THREE.Fog(color, fogParams.near, fogParams.far); } renderer.render(scene, camera); // Překreslení scény } // Fog init if (fogParams.enabled) { updateFog(); } // Adding to main GUI mainGui.add(fogParams, 'enabled').name('Fog Enabled').onChange(updateFog); mainGui.addColor(fogParams, 'color').name('Fog Color').onChange(updateFog); mainGui.add(fogParams, 'near', 0.1, 5, 0.1).name('Fog Near').onChange(updateFog); mainGui.add(fogParams, 'far', 0.1, 5, 0.1).name('Fog Far').onChange(updateFog); // Main GUI particleCount mainGui.add({ globalParticleCount: 20000 }, 'globalParticleCount', 1000, 100000).step(1000).onChange(value => { spheres.forEach((sphere, index) => { sphere.params.particleCount = value; const { newPositions, newColors, newVelocities, newBasePositions, newLifetimes, newMaxLifetimes, newBeatEffects } = reinitializeParticlesForSphere( sphere, sphere.params, sphere.geometry ); sphere.positions = newPositions; sphere.colors = newColors; sphere.velocities = newVelocities; sphere.basePositions = newBasePositions; sphere.lifetimes = newLifetimes; sphere.maxLifetimes = newMaxLifetimes; sphere.beatEffects = newBeatEffects; sphere.geometry.attributes.position.needsUpdate = true; sphere.geometry.attributes.color.needsUpdate = true; const sphereFolder = mainGui.__folders[`Sphere ${index + 1}`]; if (sphereFolder) { const particleCountController = sphereFolder.__controllers.find(controller => controller.property === 'particleCount'); if (particleCountController) { particleCountController.updateDisplay(); } } }); }); const spheres = []; function createSphereVisualization(index) { // Spheres default frequencies const defaultFrequencies = [ { minFrequency: 20, maxFrequency: 80 }, // Sub-bass { minFrequency: 120, maxFrequency: 250 }, // Bass { minFrequency: 250, maxFrequency: 800 }, // Mid { minFrequency: 1000, maxFrequency: 4000 }, // High mid { minFrequency: 5000, maxFrequency: 10000 } // High ]; const sphereParams = { enabled: index === 0, sphereRadius: 1.0, innerSphereRadius: 0.25, rotationSpeed: 0.001, rotationSpeedMin: 0, rotationSpeedMax: 0.065, rotationSmoothness: 0.3, particleCount: 20000, particleSize: 0.003, particleLifetime: 3.0, minFrequency: defaultFrequencies[index]?.minFrequency || 0, maxFrequency: defaultFrequencies[index]?.maxFrequency || 22050, minFrequencyBeat: defaultFrequencies[index]?.minFrequency || 0, maxFrequencyBeat: defaultFrequencies[index]?.maxFrequency || 22050, noiseScale: 4.0, dynamicNoiseScale: true, minNoiseScale: 0.5, maxNoiseScale: 5.0, noiseStep: 0.2, noiseSpeed: 0.1, turbulenceStrength: 0.005, colorStart: '#ff3366', colorEnd: '#3366ff', volumeChangeThreshold: 0.1, peakSensitivity: 1.1, beatThreshold: 200, baseWaveStrength: 20.0, beatStrength: 0.01, gainMultiplier: 1 }; const sphereGeometry = new THREE.BufferGeometry(); const spherePositions = new Float32Array(sphereParams.particleCount * 3); const sphereColors = new Float32Array(sphereParams.particleCount * 3); const velocities = new Float32Array(sphereParams.particleCount * 3); const basePositions = new Float32Array(sphereParams.particleCount * 3); const lifetimes = new Float32Array(sphereParams.particleCount); const maxLifetimes = new Float32Array(sphereParams.particleCount); const beatEffects = new Float32Array(sphereParams.particleCount); // Init particles for (let i = 0; i < sphereParams.particleCount; i++) { const i3 = i * 3; const radius = THREE.MathUtils.lerp(0, sphereParams.sphereRadius, sphereParams.innerSphereRadius); const theta = Math.random() * Math.PI * 2; const phi = Math.acos(2 * Math.random() - 1); const r = Math.cbrt(Math.random()) * radius; const x = r * Math.sin(phi) * Math.cos(theta); const y = r * Math.sin(phi) * Math.sin(theta); const z = r * Math.cos(phi); spherePositions[i3] = x; spherePositions[i3 + 1] = y; spherePositions[i3 + 2] = z; basePositions[i3] = x; basePositions[i3 + 1] = y; basePositions[i3 + 2] = z; velocities[i3] = 0; velocities[i3 + 1] = 0; velocities[i3 + 2] = 0; const lt = Math.random() * sphereParams.particleLifetime; lifetimes[i] = lt; maxLifetimes[i] = lt; beatEffects[i] = 0; } sphereGeometry.setAttribute('position', new THREE.BufferAttribute(spherePositions, 3)); sphereGeometry.setAttribute('color', new THREE.BufferAttribute(sphereColors, 3)); const sphereMaterial = new THREE.PointsMaterial({ size: sphereParams.particleSize, vertexColors: true, transparent: true, opacity: 0.8, blending: THREE.AdditiveBlending, fog: true }); const sphereParticleSystem = new THREE.Points(sphereGeometry, sphereMaterial); scene.add(sphereParticleSystem); // Sphere visibility `enabled` sphereParticleSystem.visible = sphereParams.enabled; const sphere = { index: index, params: sphereParams, geometry: sphereGeometry, colors: sphereColors, material: sphereMaterial, particleSystem: sphereParticleSystem, positions: spherePositions, velocities: velocities, basePositions: basePositions, lifetimes: lifetimes, maxLifetimes: maxLifetimes, beatEffects: beatEffects, lastNoiseScale: sphereParams.noiseScale, lastValidVolume: 0, lastRotationSpeed: 0 }; sphere.peakDetection = { energyHistory: [], historyLength: 30, lastPeakTime: 0, minTimeBetweenPeaks: 200 }; // Colors update updateColorsForSphere(sphereParams, sphereGeometry, sphereColors); // GUI folder const sphereFolder = mainGui.addFolder('Sphere ' + (index + 1)); sphereFolder.add(sphere.params, 'particleCount', 1000, 100000).step(1000) .onChange(() => { const { newPositions, newColors, newVelocities, newBasePositions, newLifetimes, newMaxLifetimes, newBeatEffects } = reinitializeParticlesForSphere( sphere, sphere.params, sphere.geometry ); sphere.positions = newPositions; sphere.colors = newColors; sphere.velocities = newVelocities; sphere.basePositions = newBasePositions; sphere.lifetimes = newLifetimes; sphere.maxLifetimes = newMaxLifetimes; sphere.beatEffects = newBeatEffects; sphere.geometry.attributes.position.needsUpdate = true; sphere.geometry.attributes.color.needsUpdate = true; }); sphereFolder.add(sphere.params, 'particleSize', 0.001, 0.01).step(0.001) .onChange(value => { sphere.material.size = value; }); if (index === 0) { sphereFolder.add({ copyToOthers: () => { for (let i = 1; i < spheres.length; i++) { Object.assign(spheres[i].params, JSON.parse(JSON.stringify(sphere.params))); const { newPositions, newColors, newVelocities, newBasePositions, newLifetimes, newMaxLifetimes, newBeatEffects } = reinitializeParticlesForSphere( spheres[i], spheres[i].params, spheres[i].geometry ); spheres[i].positions = newPositions; spheres[i].colors = newColors; spheres[i].velocities = newVelocities; spheres[i].basePositions = newBasePositions; spheres[i].lifetimes = newLifetimes; spheres[i].maxLifetimes = newMaxLifetimes; spheres[i].beatEffects = newBeatEffects; spheres[i].geometry.attributes.position.needsUpdate = true; spheres[i].geometry.attributes.color.needsUpdate = true; spheres[i].particleSystem.visible = spheres[i].params.enabled; const targetFolder = mainGui.__folders[`Sphere ${i + 1}`]; if (targetFolder) { targetFolder.__controllers.forEach(controller => controller.updateDisplay()); } } mainGui.updateDisplay(); console.log('Parameters copied from Sphere 1 to Spheres 2-5.'); }}, 'copyToOthers').name('Copy to Spheres 2-5'); } sphereFolder.add(sphere.params, 'particleLifetime', 1, 20).step(1); sphereFolder.add(sphere.params, 'sphereRadius', 0.05, 3.0).step(0.05); sphereFolder.add(sphere.params, 'innerSphereRadius', 0, 1).step(0.01) .onChange(() => { const { newPositions, newColors, newVelocities, newBasePositions, newLifetimes, newMaxLifetimes, newBeatEffects } = reinitializeParticlesForSphere(sphere, sphere.params, sphere.geometry); sphere.positions = newPositions; sphere.colors = newColors; sphere.velocities = newVelocities; sphere.basePositions = newBasePositions; sphere.lifetimes = newLifetimes; sphere.maxLifetimes = newMaxLifetimes; sphere.beatEffects = newBeatEffects; sphere.geometry.attributes.position.needsUpdate = true; sphere.geometry.attributes.color.needsUpdate = true; }); sphereFolder.add(sphere.params, 'rotationSpeedMin', 0, 0.02).step(0.001); sphereFolder.add(sphere.params, 'rotationSpeedMax', 0, 0.1).step(0.001); sphereFolder.add(sphere.params, 'rotationSmoothness', 0.01, 1).step(0.01); sphereFolder.add(sphere.params, 'volumeChangeThreshold', 0.01, 0.2).step(0.01); sphereFolder.add(sphereParams, 'minFrequency', 0, 22050).step(1).name('Min Frequency (Hz)') .onChange(value => sphereParams.minFrequency = value); sphereFolder.add(sphereParams, 'maxFrequency', 0, 22050).step(1).name('Max Frequency (Hz)') .onChange(value => sphereParams.maxFrequency = value); // GUI defaults const minFreqController = sphereFolder.__controllers.find(c => c.property === 'minFrequency'); const maxFreqController = sphereFolder.__controllers.find(c => c.property === 'maxFrequency'); if (minFreqController) minFreqController.setValue(sphereParams.minFrequency); if (maxFreqController) maxFreqController.setValue(sphereParams.maxFrequency); sphereFolder.add(sphere.params, 'noiseScale', 0.1, 10.0).step(0.1); sphereFolder.add(sphere.params, 'minNoiseScale', 0.0, 10.0).step(0.1).name('Min NoiseScale') .onChange(() => { if (sphere.params.minNoiseScale > sphere.params.maxNoiseScale) { sphere.params.minNoiseScale = sphere.params.maxNoiseScale; } updateNoiseStep(sphere.params); }); sphereFolder.add(sphere.params, 'maxNoiseScale', 0.0, 10.0).step(0.1).name('Max NoiseScale') .onChange(() => { if (sphere.params.maxNoiseScale < sphere.params.minNoiseScale) { sphere.params.maxNoiseScale = sphere.params.minNoiseScale; } updateNoiseStep(sphere.params); }); sphereFolder.add(sphere.params, 'noiseStep', 0.1, 5.0).step(0.1).name('Noise Step') .onChange(() => { updateNoiseStep(sphere.params); }); function updateNoiseStep(params) { const range = params.maxNoiseScale - params.minNoiseScale; if (params.noiseStep > range) { params.noiseStep = range / 2; } } sphereFolder.add(sphere.params, 'noiseSpeed', 0, 1.0).step(0.01); sphereFolder.add(sphere.params, 'peakSensitivity', 1.01, 2).step(0.01); sphereFolder.add(sphere.peakDetection, 'historyLength', 10, 1200).step(1).name('History Length'); sphereFolder.add(sphere.peakDetection, 'minTimeBetweenPeaks', 50, 5000).step(10).name('Min Time Between Peaks'); sphereFolder.add(sphere.params, 'turbulenceStrength', 0, 0.03).step(0.0001); sphereFolder.addColor(sphere.params, 'colorStart') .onChange(() => updateColorsForSphere(sphere.params, sphere.geometry, sphere.colors)); sphereFolder.addColor(sphere.params, 'colorEnd') .onChange(() => updateColorsForSphere(sphere.params, sphere.geometry, sphere.colors)); sphereFolder.add(sphereParams, 'minFrequencyBeat', 0, 22050).step(1).name('Min Freq Beat (Hz)') .onChange(value => sphereParams.minFrequencyBeat = value); sphereFolder.add(sphereParams, 'maxFrequencyBeat', 0, 22050).step(1).name('Max Freq Beat (Hz)') .onChange(value => sphereParams.maxFrequencyBeat = value); sphereFolder.add(sphere.params, 'beatThreshold', 50, 255).step(1); sphereFolder.add(sphere.params, 'beatStrength', 0, 0.05).step(0.001); sphereFolder.add(sphere.params, 'gainMultiplier', 1.0, 3.0).step(0.1); sphereFolder.add(sphere.params, 'dynamicNoiseScale'); sphereFolder.add(sphere.params, 'enabled').onChange(value => { sphere.particleSystem.visible = value; }); sphereFolder.close(); return sphere; } for (let i = 0; i < 5; i++) { const sphereVis = createSphereVisualization(i); spheres.push(sphereVis); } // Saving defaults spheres spheres.forEach(sphere => { defaultParams.push(JSON.parse(JSON.stringify(sphere.params))); }); // Init presets updatePresetOptions(); // Only sphere 1 allowed spheres.forEach((sphere, index) => { if (index !== 0) { sphere.params.enabled = false; sphere.particleSystem.visible = false; } }); function getSmoothVolume(params, lastValidVolume, volumeChangeThreshold) { if (!analyser) return { volume: 0, shouldUpdate: false }; const bufferLength = analyser.frequencyBinCount; const dataArray = new Uint8Array(bufferLength); analyser.getByteFrequencyData(dataArray); let sum = 0; for (let i = 0; i < bufferLength; i++) { sum += dataArray[i]; } const average = sum / bufferLength; const normalizedVolume = average / 255; let shouldUpdate = true; if (lastValidVolume === 0) { lastValidVolume = normalizedVolume; } else { const change = Math.abs(normalizedVolume - lastValidVolume); if (change <= volumeChangeThreshold) { lastValidVolume = normalizedVolume; } else { shouldUpdate = false; } } return { volume: lastValidVolume, shouldUpdate }; } let lastTime = 0; function animate(currentTime) { requestAnimationFrame(animate); const deltaTime = lastTime ? (currentTime - lastTime) / 1000 : 0; lastTime = currentTime; // Beat wave update beatManager.update(deltaTime); // Sphere update spheres.forEach(sphere => { if (!sphere.params.enabled) return; const audioData = getAudioData(sphere); if (audioData.peakDetected) { if (sphere.params.dynamicNoiseScale) { sphere.params.noiseScale = generateNewNoiseScale( sphere.params, sphere.lastNoiseScale ); sphere.lastNoiseScale = sphere.params.noiseScale; } } const { params, geometry, positions, velocities, basePositions, lifetimes, maxLifetimes, beatEffects } = sphere; // Beat detection sphere const beatDetected = audioData.rangeEnergyBeat > params.beatThreshold; // Beat wave sphere if (beatDetected && !beatManager.isWaveActive && params.beatStrength > 0) { beatManager.triggerWave(audioData.rangeEnergyBeat); } // Update particles const pc = params.particleCount; for (let i = 0; i < pc; i++) { const i3 = i * 3; let x = positions[i3]; let y = positions[i3 + 1]; let z = positions[i3 + 2]; let vx = velocities[i3]; let vy = velocities[i3 + 1]; let vz = velocities[i3 + 2]; let lt = lifetimes[i]; let be = beatEffects[i]; // Update lifetime lt -= deltaTime; // Noise calc const ns = params.noiseScale; const speed = params.noiseSpeed; const timeFactor = currentTime * 0.001; const noiseX = noise.noise3D(x * ns + timeFactor * speed, y * ns, z * ns); const noiseY = noise.noise3D(x * ns, y * ns + timeFactor * speed, z * ns); const noiseZ = noise.noise3D(x * ns, y * ns, z * ns + timeFactor * speed); vx += noiseX * params.turbulenceStrength; vy += noiseY * params.turbulenceStrength; vz += noiseZ * params.turbulenceStrength; // Beat effect if (beatDetected) { be = 1.0; } be *= 0.95; if (be > 0.01) { // směr z centra const dist = Math.sqrt(x*x + y*y + z*z); if (dist > 0) { const dx = x / dist; const dy = y / dist; const dz = z / dist; const beatForce = be * params.beatStrength; vx += dx * beatForce; vy += dy * beatForce; vz += dz * beatForce; } } // Update positions x += vx; y += vy; z += vz; // Slowing speed vx *= 0.98; vy *= 0.98; vz *= 0.98; // Radius control const dist = Math.sqrt(x*x + y*y + z*z); if (dist > params.sphereRadius) { const overflow = dist - params.sphereRadius; const pullback = overflow * 0.1; if (dist > 0) { const dx = x / dist; const dy = y / dist; const dz = z / dist; x -= dx * pullback; y -= dy * pullback; z -= dz * pullback; } vx *= 0.9; vy *= 0.9; vz *= 0.9; } // Reset of dead particles if (lt <= 0) { const radius = THREE.MathUtils.lerp(0, params.sphereRadius, params.innerSphereRadius); const theta = Math.random() * Math.PI * 2; const phi = Math.acos(2 * Math.random() - 1); const rr = Math.cbrt(Math.random()) * radius; x = rr * Math.sin(phi) * Math.cos(theta); y = rr * Math.sin(phi) * Math.sin(theta); z = rr * Math.cos(phi); vx = 0; vy = 0; vz = 0; const newLt = Math.random() * params.particleLifetime; lt = newLt; maxLifetimes[i] = newLt; be = 0; basePositions[i3] = x; basePositions[i3 + 1] = y; basePositions[i3 + 2] = z; } positions[i3] = x; positions[i3 + 1] = y; positions[i3 + 2] = z; velocities[i3] = vx; velocities[i3 + 1] = vy; velocities[i3 + 2] = vz; lifetimes[i] = lt; beatEffects[i] = be; } geometry.attributes.position.needsUpdate = true; // Dyn rotation const { volume: smoothVolume, shouldUpdate } = getSmoothVolume( params, sphere.lastValidVolume, params.volumeChangeThreshold ); if (shouldUpdate) { const targetRotationSpeed = THREE.MathUtils.lerp( params.rotationSpeedMin, params.rotationSpeedMax, smoothVolume ); sphere.lastRotationSpeed = params.rotationSpeed + (targetRotationSpeed - params.rotationSpeed) * params.rotationSmoothness; } sphere.particleSystem.rotation.y += sphere.lastRotationSpeed; // Saving volume if (shouldUpdate) sphere.lastValidVolume = smoothVolume; }); renderer.render(scene, camera); updateTimeline(); } window.addEventListener('resize', () => { renderer.setSize(window.innerWidth, window.innerHeight); camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); }); let controlsVisible = true; // Stav viditelnosti // Switching control elements function toggleControlsVisibility() { controlsVisible = !controlsVisible; const allControls = document.querySelectorAll( '.controls-container, .dg.main, #audioControls, #presetContainer, #songSelect, #playPause, input[type="range"], button, select, input[type="text"]' ); allControls.forEach(control => { control.style.display = controlsVisible ? 'block' : 'none'; }); } // Event listener on click document.addEventListener('click', (event) => { const clickedElement = event.target; if (!clickedElement.closest('.controls-container') && !clickedElement.closest('.dg.main') && !clickedElement.closest('#audioControls') && !clickedElement.closest('#presetContainer') && !clickedElement.closest('#songSelect') && !clickedElement.closest('#playPause') && !clickedElement.closest('input[type="range"]') && !clickedElement.closest('button') && !clickedElement.closest('select') && !clickedElement.closest('input[type="text"]')) { toggleControlsVisibility(); } }); document.querySelectorAll('.controls-container, .dg.main, #audioControls, #presetContainer').forEach(control => { control.style.position = 'absolute'; }); console.log('Starting animation'); initAudio(); animate(0);