Spaces:
Running
Running
| <html lang="ru"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Audio Visualizer</title> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| background: #0f0f1a; | |
| color: #fff; | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| overflow-x: hidden; | |
| } | |
| .container { | |
| width: 100%; | |
| min-height: 100vh; | |
| padding: 20px; | |
| position: relative; | |
| } | |
| header { | |
| text-align: center; | |
| padding: 40px 0; | |
| animation: fadeIn 1s ease-in; | |
| } | |
| h1 { | |
| font-size: 3em; | |
| margin-bottom: 20px; | |
| background: linear-gradient(45deg, #ff6b6b, #4ecdc4); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| } | |
| .visualizer-container { | |
| width: 100%; | |
| height: calc(100vh - 200px); | |
| margin: 0 auto; | |
| background: rgba(255, 255, 255, 0.05); | |
| border-radius: 15px; | |
| padding: 20px; | |
| position: relative; | |
| } | |
| canvas { | |
| width: 100%; | |
| height: 100%; | |
| border-radius: 10px; | |
| background: rgba(0, 0, 0, 0.3); | |
| } | |
| .controls { | |
| position: fixed; | |
| bottom: 20px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| display: flex; | |
| gap: 10px; | |
| flex-wrap: wrap; | |
| justify-content: center; | |
| z-index: 100; | |
| } | |
| button { | |
| padding: 10px 20px; | |
| border: none; | |
| border-radius: 25px; | |
| background: linear-gradient(45deg, #ff6b6b, #4ecdc4); | |
| color: white; | |
| cursor: pointer; | |
| transition: transform 0.2s; | |
| } | |
| button:hover { | |
| transform: scale(1.05); | |
| } | |
| input[type="file"] { | |
| display: none; | |
| } | |
| .visualization-styles { | |
| position: fixed; | |
| top: 20px; | |
| right: 20px; | |
| display: flex; | |
| gap: 10px; | |
| flex-direction: column; | |
| z-index: 100; | |
| } | |
| .style-option { | |
| padding: 8px 15px; | |
| border-radius: 15px; | |
| background: rgba(255, 255, 255, 0.1); | |
| cursor: pointer; | |
| transition: background 0.3s; | |
| } | |
| .style-option:hover { | |
| background: rgba(255, 255, 255, 0.2); | |
| } | |
| .center-image { | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| max-width: 200px; | |
| max-height: 200px; | |
| z-index: 10; | |
| } | |
| .background-image { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| object-fit: cover; | |
| opacity: 0.3; | |
| z-index: 1; | |
| } | |
| .fullscreen-btn { | |
| position: fixed; | |
| top: 20px; | |
| left: 20px; | |
| z-index: 100; | |
| } | |
| .credit { | |
| position: fixed; | |
| bottom: 10px; | |
| right: 20px; | |
| font-size: 14px; | |
| opacity: 0.7; | |
| z-index: 100; | |
| } | |
| @keyframes fadeIn { | |
| from { | |
| opacity: 0; | |
| transform: translateY(-20px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| @media (max-width: 768px) { | |
| .visualizer-container { | |
| padding: 10px; | |
| } | |
| h1 { | |
| font-size: 2em; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <header> | |
| <h1>Audio Visualizer</h1> | |
| </header> | |
| <div class="visualizer-container" id="visualizer-container"> | |
| <canvas id="visualizer"></canvas> | |
| <img id="centerImage" class="center-image" style="display: none;"> | |
| <img id="backgroundImage" class="background-image" style="display: none;"> | |
| </div> | |
| <div class="controls"> | |
| <button onclick="document.getElementById('audioInput').click()">Upload Audio</button> | |
| <button onclick="document.getElementById('imageInput').click()">Upload Center Image</button> | |
| <button onclick="document.getElementById('bgImageInput').click()">Upload Background</button> | |
| <button id="playPause">Play</button> | |
| <input type="file" id="audioInput" accept="audio/*"> | |
| <input type="file" id="imageInput" accept="image/*,video/gif"> | |
| <input type="file" id="bgImageInput" accept="image/*,video/gif"> | |
| </div> | |
| <button class="fullscreen-btn" onclick="toggleFullscreen()">Fullscreen</button> | |
| <div class="visualization-styles"> | |
| <div class="style-option" data-style="bars">Bars</div> | |
| <div class="style-option" data-style="wave">Wave</div> | |
| <div class="style-option" data-style="circle">Circle</div> | |
| </div> | |
| <div class="credit">by Софронов Артемий</div> | |
| </div> | |
| <script> | |
| let audioContext, analyser, source; | |
| const canvas = document.getElementById('visualizer'); | |
| const ctx = canvas.getContext('2d'); | |
| let animationId; | |
| let currentStyle = 'bars'; | |
| let centerImage = document.getElementById('centerImage'); | |
| let backgroundImage = document.getElementById('backgroundImage'); | |
| let bassValue = 0; | |
| function resizeCanvas() { | |
| canvas.width = canvas.clientWidth * window.devicePixelRatio; | |
| canvas.height = canvas.clientHeight * window.devicePixelRatio; | |
| } | |
| resizeCanvas(); | |
| window.addEventListener('resize', resizeCanvas); | |
| document.getElementById('audioInput').addEventListener('change', function(e) { | |
| const file = e.target.files[0]; | |
| const audio = new Audio(); | |
| audio.src = URL.createObjectURL(file); | |
| setupAudioContext(audio); | |
| }); | |
| document.getElementById('imageInput').addEventListener('change', function(e) { | |
| const file = e.target.files[0]; | |
| centerImage.src = URL.createObjectURL(file); | |
| centerImage.style.display = 'block'; | |
| }); | |
| document.getElementById('bgImageInput').addEventListener('change', function(e) { | |
| const file = e.target.files[0]; | |
| backgroundImage.src = URL.createObjectURL(file); | |
| backgroundImage.style.display = 'block'; | |
| }); | |
| function setupAudioContext(audio) { | |
| if (audioContext) audioContext.close(); | |
| audioContext = new (window.AudioContext || window.webkitAudioContext)(); | |
| analyser = audioContext.createAnalyser(); | |
| source = audioContext.createMediaElementSource(audio); | |
| source.connect(analyser); | |
| analyser.connect(audioContext.destination); | |
| analyser.fftSize = 256; | |
| audio.play(); | |
| draw(); | |
| } | |
| function draw() { | |
| const bufferLength = analyser.frequencyBinCount; | |
| const dataArray = new Uint8Array(bufferLength); | |
| function animate() { | |
| animationId = requestAnimationFrame(animate); | |
| analyser.getByteFrequencyData(dataArray); | |
| ctx.fillStyle = 'rgba(15, 15, 26, 0.2)'; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| // Get bass value for image animation | |
| bassValue = dataArray.slice(0, 10).reduce((a, b) => a + b) / 10; | |
| if (centerImage.style.display !== 'none') { | |
| const scale = 1 + (bassValue / 255) * 0.2; | |
| centerImage.style.transform = `translate(-50%, -50%) scale(${scale})`; | |
| } | |
| switch(currentStyle) { | |
| case 'bars': | |
| drawBars(dataArray, bufferLength); | |
| break; | |
| case 'wave': | |
| drawWave(dataArray, bufferLength); | |
| break; | |
| case 'circle': | |
| drawCircle(dataArray, bufferLength); | |
| break; | |
| } | |
| } | |
| animate(); | |
| } | |
| function drawBars(dataArray, bufferLength) { | |
| const barWidth = canvas.width / bufferLength; | |
| for(let i = 0; i < bufferLength; i++) { | |
| const barHeight = dataArray[i] * canvas.height / 255; | |
| const gradient = ctx.createLinearGradient(0, canvas.height, 0, 0); | |
| gradient.addColorStop(0, '#ff6b6b'); | |
| gradient.addColorStop(1, '#4ecdc4'); | |
| ctx.fillStyle = gradient; | |
| ctx.fillRect(i * barWidth, canvas.height - barHeight, barWidth - 1, barHeight); | |
| } | |
| } | |
| function drawWave(dataArray, bufferLength) { | |
| ctx.beginPath(); | |
| ctx.strokeStyle = '#4ecdc4'; | |
| ctx.lineWidth = 2; | |
| for(let i = 0; i < bufferLength; i++) { | |
| const x = i * (canvas.width / bufferLength); | |
| const y = (dataArray[i] / 128.0) * (canvas.height / 2); | |
| if(i === 0) { | |
| ctx.moveTo(x, y); | |
| } else { | |
| ctx.lineTo(x, y); | |
| } | |
| } | |
| ctx.stroke(); | |
| } | |
| function drawCircle(dataArray, bufferLength) { | |
| const centerX = canvas.width / 2; | |
| const centerY = canvas.height / 2; | |
| const radius = Math.min(centerX, centerY) - 50; | |
| ctx.beginPath(); | |
| ctx.strokeStyle = '#ff6b6b'; | |
| ctx.lineWidth = 2; | |
| for(let i = 0; i < bufferLength; i++) { | |
| const angle = i * 2 * Math.PI / bufferLength; | |
| const amplitude = (dataArray[i] * radius / 255) + radius; | |
| const x = centerX + amplitude * Math.cos(angle); | |
| const y = centerY + amplitude * Math.sin(angle); | |
| if(i === 0) { | |
| ctx.moveTo(x, y); | |
| } else { | |
| ctx.lineTo(x, y); | |
| } | |
| } | |
| ctx.closePath(); | |
| ctx.stroke(); | |
| // Draw base circle | |
| ctx.beginPath(); | |
| ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI); | |
| ctx.strokeStyle = 'rgba(255, 107, 107, 0.3)'; | |
| ctx.stroke(); | |
| } | |
| document.querySelectorAll('.style-option').forEach(option => { | |
| option.addEventListener('click', function() { | |
| currentStyle = this.dataset.style; | |
| }); | |
| }); | |
| function toggleFullscreen() { | |
| if (!document.fullscreenElement) { | |
| document.documentElement.requestFullscreen(); | |
| } else { | |
| document.exitFullscreen(); | |
| } | |
| } | |
| </script> | |
| </body> | |
| </html> |