Spaces:
Running
Running
| <html lang="de"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Ferrofluid Audio Visualizer</title> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| background: #000; | |
| font-family: 'Arial', sans-serif; | |
| overflow: hidden; | |
| color: white; | |
| } | |
| .header { | |
| position: absolute; | |
| top: 0; | |
| width: 100%; | |
| padding: 20px; | |
| z-index: 100; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| background: rgba(0, 0, 0, 0.5); | |
| } | |
| .header h1 { | |
| font-size: 1.5rem; | |
| } | |
| .anycoder-link { | |
| color: #00ff88; | |
| text-decoration: none; | |
| font-size: 0.9rem; | |
| transition: color 0.3s; | |
| } | |
| .anycoder-link:hover { | |
| color: #00cc66; | |
| } | |
| .container { | |
| position: relative; | |
| width: 100vw; | |
| height: 100vh; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| } | |
| #visualizer { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| } | |
| .controls { | |
| position: absolute; | |
| bottom: 30px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| z-index: 100; | |
| background: rgba(0, 0, 0, 0.7); | |
| padding: 20px; | |
| border-radius: 10px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 15px; | |
| align-items: center; | |
| min-width: 300px; | |
| } | |
| .file-input-container { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .file-input-label { | |
| background: #00ff88; | |
| color: black; | |
| padding: 10px 20px; | |
| border-radius: 5px; | |
| cursor: pointer; | |
| font-weight: bold; | |
| transition: background 0.3s; | |
| } | |
| .file-input-label:hover { | |
| background: #00cc66; | |
| } | |
| #audioFile { | |
| display: none; | |
| } | |
| .button-group { | |
| display: flex; | |
| gap: 10px; | |
| } | |
| button { | |
| background: #333; | |
| color: white; | |
| border: none; | |
| padding: 10px 20px; | |
| border-radius: 5px; | |
| cursor: pointer; | |
| transition: background 0.3s; | |
| } | |
| button:hover { | |
| background: #555; | |
| } | |
| button:disabled { | |
| background: #222; | |
| color: #666; | |
| cursor: not-allowed; | |
| } | |
| .slider-container { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| width: 100%; | |
| } | |
| .slider-container label { | |
| min-width: 80px; | |
| } | |
| input[type="range"] { | |
| flex: 1; | |
| height: 5px; | |
| background: #333; | |
| outline: none; | |
| border-radius: 5px; | |
| } | |
| .status { | |
| font-size: 0.9rem; | |
| color: #00ff88; | |
| } | |
| @media (max-width: 768px) { | |
| .controls { | |
| width: 90%; | |
| min-width: unset; | |
| } | |
| .header h1 { | |
| font-size: 1.2rem; | |
| } | |
| .button-group { | |
| flex-direction: column; | |
| width: 100%; | |
| } | |
| button { | |
| width: 100%; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="header"> | |
| <h1>Ferrofluid Audio Visualizer</h1> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" class="anycoder-link" target="_blank">Built with anycoder</a> | |
| </div> | |
| <div class="container"> | |
| <canvas id="visualizer"></canvas> | |
| <div class="controls"> | |
| <div class="file-input-container"> | |
| <label for="audioFile" class="file-input-label">Audio Datei auswählen</label> | |
| <input type="file" id="audioFile" accept="audio/*"> | |
| <div class="status" id="fileStatus">Keine Datei ausgewählt</div> | |
| </div> | |
| <div class="slider-container"> | |
| <label for="sensitivity">Empfindlichkeit:</label> | |
| <input type="range" id="sensitivity" min="1" max="10" value="5"> | |
| </div> | |
| <div class="slider-container"> | |
| <label for="smoothness">Glättung:</label> | |
| <input type="range" id="smoothness" min="1" max="10" value="3"> | |
| </div> | |
| <div class="button-group"> | |
| <button id="playBtn" disabled>Abspielen</button> | |
| <button id="pauseBtn" disabled>Pause</button> | |
| <button id="stopBtn" disabled>Stop</button> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Canvas und Audio Kontext Setup | |
| const canvas = document.getElementById('visualizer'); | |
| const ctx = canvas.getContext('2d'); | |
| const audioContext = new (window.AudioContext || window.webkitAudioContext)(); | |
| // DOM Elemente | |
| const audioFileInput = document.getElementById('audioFile'); | |
| const playBtn = document.getElementById('playBtn'); | |
| const pauseBtn = document.getElementById('pauseBtn'); | |
| const stopBtn = document.getElementById('stopBtn'); | |
| const sensitivitySlider = document.getElementById('sensitivity'); | |
| const smoothnessSlider = document.getElementById('smoothness'); | |
| const fileStatus = document.getElementById('fileStatus'); | |
| // Variablen | |
| let audioSource; | |
| let analyser; | |
| let dataArray; | |
| let bufferLength; | |
| let animationId; | |
| let isPlaying = false; | |
| let audioBuffer; | |
| let rotationX = 0; | |
| let rotationY = 0; | |
| // Canvas Größe anpassen | |
| function resizeCanvas() { | |
| canvas.width = window.innerWidth; | |
| canvas.height = window.innerHeight; | |
| } | |
| window.addEventListener('resize', resizeCanvas); | |
| resizeCanvas(); | |
| // Audio File Handling | |
| audioFileInput.addEventListener('change', function(e) { | |
| const file = e.target.files[0]; | |
| if (!file) return; | |
| fileStatus.textContent = `Geladen: ${file.name}`; | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| const arrayBuffer = e.target.result; | |
| audioContext.decodeAudioData(arrayBuffer, function(buffer) { | |
| audioBuffer = buffer; | |
| setupAudioAnalysis(); | |
| playBtn.disabled = false; | |
| }, function(e) { | |
| console.error('Error decoding audio data', e); | |
| fileStatus.textContent = 'Fehler beim Laden der Audio-Datei'; | |
| }); | |
| }; | |
| reader.readAsArrayBuffer(file); | |
| }); | |
| // Audio Analyse Setup | |
| function setupAudioAnalysis() { | |
| if (analyser) { | |
| analyser.disconnect(); | |
| } | |
| analyser = audioContext.createAnalyser(); | |
| analyser.fftSize = 256; | |
| bufferLength = analyser.frequencyBinCount; | |
| dataArray = new Uint8Array(bufferLength); | |
| // Smoothing-Faktor basierend auf Slider | |
| analyser.smoothingTimeConstant = (11 - smoothnessSlider.value) / 10; | |
| } | |
| // Ferrofluid Visualisierung | |
| function visualize() { | |
| if (!isPlaying || !analyser) return; | |
| analyser.getByteFrequencyData(dataArray); | |
| ctx.fillStyle = 'rgba(0, 0, 0, 0.1)'; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| const centerX = canvas.width / 2; | |
| const centerY = canvas.height / 2; | |
| const baseRadius = Math.min(canvas.width, canvas.height) / 4; | |
| const sensitivity = sensitivitySlider.value / 2; | |
| ctx.save(); | |
| ctx.translate(centerX, centerY); | |
| // 3D-Rotation basierend auf Audio-Daten | |
| rotationX += dataArray[10] / 1000; | |
| rotationY += dataArray[20] / 800; | |
| ctx.rotateX(rotationX); | |
| ctx.rotateY(rotationY); | |
| // Ferrofluid-ähnliche Blob-Form zeichnen | |
| ctx.beginPath(); | |
| for (let i = 0; i < bufferLength; i++) { | |
| const amplitude = dataArray[i] / 255; | |
| const angle = (i / bufferLength) * Math.PI * 2; | |
| const radius = baseRadius + (amplitude * baseRadius * sensitivity); | |
| const x = Math.cos(angle) * radius; | |
| const y = Math.sin(angle) * radius; | |
| // Z-Koordinate für 3D-Effekt | |
| const z = Math.sin(angle * 2) * amplitude * 50; | |
| if (i === 0) { | |
| ctx.moveTo(x, y + z); | |
| } else { | |
| ctx.lineTo(x, y + z); | |
| } | |
| } | |
| ctx.closePath(); | |
| // Ferrofluid-ähnlicher Gradient | |
| const gradient = ctx.createRadialGradient(0, 0, 0, 0, 0, baseRadius * 2); | |
| gradient.addColorStop(0, 'rgba(0, 255, 136, 0.8)'); | |
| gradient.addColorStop(0.5, 'rgba(0, 200, 100, 0.6)'); | |
| gradient.addColorStop(1, 'rgba(0, 100, 50, 0.4)'); | |
| ctx.fillStyle = gradient; | |
| ctx.fill(); | |
| // Glänzende Highlights | |
| ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)'; | |
| ctx.lineWidth = 2; | |
| ctx.stroke(); | |
| ctx.restore(); | |
| animationId = requestAnimationFrame(visualize); | |
| } | |
| // Steuerung | |
| playBtn.addEventListener('click', function() { | |
| if (!audioBuffer) return; | |
| if (audioContext.state === 'suspended') { | |
| audioContext.resume(); | |
| } | |
| if (audioSource) { | |
| audioSource.stop(); | |
| } | |
| audioSource = audioContext.createBufferSource(); | |
| audioSource.buffer = audioBuffer; | |
| audioSource.connect(analyser); | |
| analyser.connect(audioContext.destination); | |
| audioSource.start(0); | |
| isPlaying = true; | |
| playBtn.disabled = true; | |
| pauseBtn.disabled = false; | |
| stopBtn.disabled = false; | |
| visualize(); | |
| }); | |
| pauseBtn.addEventListener('click', function() { | |
| if (audioContext.state === 'running') { | |
| audioContext.suspend().then(function() { | |
| isPlaying = false; | |
| playBtn.disabled = false; | |
| pauseBtn.disabled = true; | |
| cancelAnimationFrame(animationId); | |
| }); | |
| } | |
| }); | |
| stopBtn.addEventListener('click', function() { | |
| if (audioSource) { | |
| audioSource.stop(); | |
| audioSource = null; | |
| } | |
| isPlaying = false; | |
| playBtn.disabled = false; | |
| pauseBtn.disabled = true; | |
| stopBtn.disabled = true; | |
| cancelAnimationFrame(animationId); | |
| // Canvas zurücksetzen | |
| ctx.fillStyle = 'black'; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| }); | |
| // Slider-Event-Listener | |
| smoothnessSlider.addEventListener('input', function() { | |
| if (analyser) { | |
| analyser.smoothingTimeConstant = (11 - this.value) / 10; | |
| } | |
| }); | |
| // Maus-Interaktion für zusätzliche Rotation | |
| let mouseX = 0; | |
| let mouseY = 0; | |
| canvas.addEventListener('mousemove', function(e) { | |
| if (!isPlaying) return; | |
| mouseX = (e.clientX / canvas.width - 0.5) * 2; | |
| mouseY = (e.clientY / canvas.height - 0.5) * 2; | |
| }); | |
| // Touch-Interaktion für Mobile | |
| canvas.addEventListener('touchmove', function(e) { | |
| if (!isPlaying) return; | |
| e.preventDefault(); | |
| const touch = e.touches[0]; | |
| mouseX = (touch.clientX / canvas.width - 0.5) * 2; | |
| mouseY = (touch.clientY / canvas.height - 0.5) * 2; | |
| }, { passive: false }); | |
| // Initialen schwarzen Hintergrund zeichnen | |
| ctx.fillStyle = 'black'; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| </script> | |
| </body> | |
| </html> |