Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> | |
| <title>Baarons Beats | Bubble Drum Pad</title> | |
| <style> | |
| :root { | |
| --ios-white: #fbfbfd; | |
| --ios-glass: rgba(255, 255, 255, 0.7); | |
| --ios-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); | |
| --accent-blue: #007aff; | |
| --accent-red: #ff3b30; | |
| } | |
| body { | |
| margin: 0; | |
| padding: 0; | |
| background-color: #f2f2f7; | |
| background-image: radial-gradient(circle at top right, #ffffff, #e5e5e5); | |
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; | |
| color: #1d1d1f; | |
| min-height: 100vh; | |
| overflow-x: hidden; | |
| } | |
| /* Splash / App Launcher Overlay */ | |
| #appLauncher { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: #fff; | |
| z-index: 1000; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| transition: opacity 0.5s ease, visibility 0.5s; | |
| } | |
| .app-icon-large { | |
| width: 120px; | |
| height: 120px; | |
| background: linear-gradient(145deg, #007aff, #0056b3); | |
| border-radius: 28px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| box-shadow: 0 20px 40px rgba(0,122,255,0.3); | |
| margin-bottom: 24px; | |
| } | |
| .app-icon-large span { | |
| color: white; | |
| font-size: 40px; | |
| font-weight: bold; | |
| } | |
| .launch-btn { | |
| background: var(--accent-blue); | |
| color: white; | |
| border: none; | |
| padding: 16px 32px; | |
| border-radius: 18px; | |
| font-size: 18px; | |
| font-weight: 600; | |
| cursor: pointer; | |
| box-shadow: 0 4px 12px rgba(0,122,255,0.2); | |
| } | |
| /* Main Container */ | |
| .app-container { | |
| max-width: 1000px; | |
| margin: 20px auto; | |
| padding: 20px; | |
| background: var(--ios-glass); | |
| backdrop-filter: blur(20px); | |
| -webkit-backdrop-filter: blur(20px); | |
| border-radius: 40px; | |
| border: 1px solid rgba(255, 255, 255, 0.8); | |
| box-shadow: var(--ios-shadow); | |
| position: relative; | |
| display: none; /* Hidden until "Open" */ | |
| } | |
| .logo-circle { | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| width: 180px; | |
| height: 180px; | |
| background: white; | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| text-align: center; | |
| box-shadow: 0 10px 25px rgba(0,0,0,0.05); | |
| z-index: 10; | |
| border: 1px solid rgba(0,0,0,0.03); | |
| pointer-events: none; | |
| } | |
| .logo-text { | |
| font-weight: 700; | |
| text-transform: lowercase; | |
| font-size: 1.2rem; | |
| letter-spacing: -0.5px; | |
| color: #333; | |
| } | |
| header h1 { | |
| text-align: center; | |
| font-weight: 600; | |
| font-size: 2rem; | |
| margin: 10px 0 20px 0; | |
| letter-spacing: -1px; | |
| } | |
| .canvas-area { | |
| position: relative; | |
| width: 100%; | |
| height: 500px; | |
| margin-bottom: 30px; | |
| background: rgba(255, 255, 255, 0.4); | |
| border-radius: 30px; | |
| overflow: hidden; | |
| border: 1px solid rgba(255, 255, 255, 0.5); | |
| touch-action: none; | |
| } | |
| .bubble { | |
| position: absolute; | |
| width: 28px; | |
| height: 28px; | |
| border-radius: 50%; | |
| background: rgba(0, 122, 255, 0.15); | |
| border: 1.5px solid rgba(0, 122, 255, 0.3); | |
| cursor: pointer; | |
| transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| user-select: none; | |
| } | |
| .bubble.active { | |
| animation: flash 0.3s ease-out; | |
| background: var(--accent-blue) ; | |
| box-shadow: 0 0 20px rgba(0, 122, 255, 0.6); | |
| } | |
| @keyframes flash { | |
| 0% { transform: scale(1); opacity: 1; } | |
| 50% { transform: scale(1.6); opacity: 0.7; } | |
| 100% { transform: scale(1); opacity: 1; } | |
| } | |
| .controls-bar { | |
| position: absolute; | |
| bottom: 20px; | |
| left: 5%; | |
| width: 90%; | |
| display: flex; | |
| gap: 8px; | |
| background: rgba(255, 255, 255, 0.85); | |
| padding: 12px; | |
| border-radius: 20px; | |
| box-shadow: 0 4px 15px rgba(0,0,0,0.05); | |
| backdrop-filter: blur(10px); | |
| z-index: 20; | |
| overflow-x: auto; | |
| scrollbar-width: none; | |
| } | |
| .control-btn { | |
| background: #e9e9eb; | |
| color: #1d1d1f; | |
| border: none; | |
| padding: 10px 16px; | |
| border-radius: 12px; | |
| font-weight: 500; | |
| font-size: 13px; | |
| cursor: pointer; | |
| white-space: nowrap; | |
| } | |
| .control-btn.active { | |
| background: var(--accent-red); | |
| color: white; | |
| } | |
| .upload-section { | |
| background: white; | |
| border-radius: 24px; | |
| padding: 20px; | |
| margin-top: 20px; | |
| } | |
| .file-input-label { | |
| background: var(--accent-blue); | |
| color: white; | |
| padding: 10px 20px; | |
| border-radius: 14px; | |
| cursor: pointer; | |
| display: inline-block; | |
| font-weight: 500; | |
| } | |
| #mp3UploadInput { display: none; } | |
| .song-item { | |
| background: #f2f2f7; | |
| padding: 6px 12px; | |
| border-radius: 10px; | |
| font-size: 11px; | |
| display: inline-flex; | |
| align-items: center; | |
| margin: 4px; | |
| } | |
| .song-color { width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; } | |
| footer { text-align: center; margin: 20px 0; opacity: 0.4; font-size: 11px; } | |
| @media (max-width: 600px) { | |
| .app-container { margin: 0; border-radius: 0; min-height: 100vh; } | |
| .canvas-area { height: 60vh; } | |
| .logo-circle { width: 120px; height: 120px; font-size: 0.8rem; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="appLauncher"> | |
| <div class="app-icon-large"><span>BB</span></div> | |
| <h2 style="margin-bottom: 30px; font-weight: 600;">Baarons Beats</h2> | |
| <button class="launch-btn" onclick="openInApp()">Open in App</button> | |
| </div> | |
| <div class="app-container" id="mainApp"> | |
| <header> | |
| <h1>baarons beats</h1> | |
| </header> | |
| <section class="canvas-area"> | |
| <div class="logo-circle"> | |
| <div class="logo-text">baarons<br>beats</div> | |
| </div> | |
| <div id="bubblesCtn" style="position:absolute; top:0; left:0; width:100%; height:100%;"></div> | |
| <div class="controls-bar"> | |
| <button id="playBtn" class="control-btn">Play</button> | |
| <button id="recordBtn" class="control-btn">Record</button> | |
| <button id="stopBtn" class="control-btn">Stop</button> | |
| <button id="loopBtn" class="control-btn">Loop</button> | |
| <button id="structureBtn" class="control-btn">Layout</button> | |
| <select id="patternSelect" style="border-radius:10px; border:none; background:#e9e9eb; padding:5px 10px;"> | |
| <option value="">Pattern</option> | |
| <option value="rock">Rock</option> | |
| <option value="hiphop">Hip-Hop</option> | |
| <option value="house">House</option> | |
| </select> | |
| <div style="display:flex; align-items:center; gap:8px; font-size:12px; padding-left: 10px;"> | |
| <input type="range" id="tempoSlider" min="60" max="240" value="120" style="width: 80px;"> | |
| <span id="tempoValue">120</span> | |
| </div> | |
| </div> | |
| </section> | |
| <section class="upload-section"> | |
| <h3 style="margin-top:0; font-size: 16px;">Sound Engine</h3> | |
| <label for="mp3UploadInput" class="file-input-label">Upload Samples</label> | |
| <input type="file" id="mp3UploadInput" multiple accept="audio/mp3"> | |
| <button id="resetBubblesBtn" class="control-btn" style="float:right">Reset</button> | |
| <div id="uploadedSongsCtn" style="margin-top:15px;"></div> | |
| </section> | |
| <footer> | |
| © 2026 Baarons Beats | |
| </footer> | |
| </div> | |
| <script> | |
| const drumSounds = ["Kick", "Snare", "Hi-Hat", "Crash", "Tom", "Clap", "Ride", "Cowbell", "Bass", "Perc", "Shaker", "Tamb", "Cymbal", "Conga", "Bongo", "Triangle", "Wood", "Metal", "Glass", "Water", "Wind", "Thunder", "Fire", "Ice", "Electric", "Acoustic", "Synth", "Analog", "Digital", "Vintage", "Modern", "Futuristic", "Soft", "Hard", "Sharp", "Mellow", "Bright", "Dark", "Warm", "Cool", "Short", "Long", "Fast", "Slow", "High", "Low", "Mid", "Full", "Punchy", "Smooth", "Rough", "Clean", "Dirty", "Wet", "Dry", "Echo", "Reverb", "Delay", "Flange", "Phaser", "Chorus", "Distort", "Boost", "Cut", "Beat", "Rhythm", "Tempo", "Groove", "Swing", "Sync", "Pulse", "Vibe", "Hit", "Strike", "Tap", "Slap", "Bang", "Crack", "Pop", "Snap", "Boom", "Bap", "Tss", "Chk", "Clack", "Click", "Clunk", "Thud", "Zap", "Zing", "Zip", "Zoom", "Whiz", "Wham", "Pow", "Bam", "Whack", "Smash", "Clash", "Bash", "Ting", "Ping", "Pong", "Ding", "Dong", "Bing", "Bong", "Ring", "Tock", "Tick", "Tack", "Tuk", "Tok", "Tak", "Tek", "Tik", "Drum", "Pad", "Machine", "Beatbox", "Sampler", "Sequencer", "Mixer", "Console", "Audio", "Sound", "Wave", "Tone", "Pitch", "Volume", "Pan", "Effect", "Loop", "Pattern", "Sequence", "Track", "Channel", "Bus", "Send", "Return", "Input", "Output", "Mono", "Stereo", "Surround", "Spatial", "3D", "Ambient"]; | |
| let audioContext = null; | |
| let isRecording = false; | |
| let isPlaying = false; | |
| let isLooping = false; | |
| let recordedEvents = []; | |
| let recordStartTime = 0; | |
| let playbackTimeout = null; | |
| let currentTempo = 120; | |
| let bubbles = []; | |
| let currentLayout = 'circle'; | |
| let uploadedSongs = []; | |
| let bubbleSnippets = []; | |
| function openInApp() { | |
| // Initialize Audio Context on User Gesture | |
| audioContext = new (window.AudioContext || window.webkitAudioContext)(); | |
| document.getElementById('appLauncher').style.opacity = '0'; | |
| setTimeout(() => { | |
| document.getElementById('appLauncher').style.visibility = 'hidden'; | |
| document.getElementById('mainApp').style.display = 'block'; | |
| initBubbles(); | |
| }, 500); | |
| } | |
| function initBubbles() { | |
| const container = document.getElementById('bubblesCtn'); | |
| const centerX = container.offsetWidth / 2; | |
| const centerY = container.offsetHeight / 2; | |
| const maxRadius = Math.min(centerX, centerY) - 40; | |
| const rings = 7; | |
| const bubblesPerRing = Math.ceil(drumSounds.length / rings); | |
| container.innerHTML = ''; | |
| bubbles = []; | |
| bubbleSnippets = []; | |
| for (let i = 0; i < drumSounds.length; i++) { | |
| const ring = Math.floor(i / bubblesPerRing); | |
| const posInRing = i % bubblesPerRing; | |
| const radius = (ring + 1) * (maxRadius / rings); | |
| const angle = (posInRing / bubblesPerRing) * Math.PI * 2; | |
| const x = centerX + radius * Math.cos(angle); | |
| const y = centerY + radius * Math.sin(angle); | |
| const bubble = document.createElement('div'); | |
| bubble.className = 'bubble'; | |
| bubble.style.left = `${x - 14}px`; | |
| bubble.style.top = `${y - 14}px`; | |
| bubble.dataset.index = i; | |
| bubble.dataset.frequency = 80 + Math.random() * 800; | |
| bubble.dataset.duration = 0.1 + Math.random() * 0.4; | |
| bubble.dataset.waveform = ['sine', 'triangle'][Math.floor(Math.random()*2)]; | |
| const triggerSound = () => { | |
| playBubbleSound(bubble); | |
| if (isRecording) { | |
| const timeSinceStart = audioContext.currentTime - recordStartTime; | |
| recordedEvents.push({ time: timeSinceStart, bubbleIndex: i }); | |
| } | |
| }; | |
| bubble.onmousedown = triggerSound; | |
| bubble.ontouchstart = (e) => { | |
| e.preventDefault(); | |
| triggerSound(); | |
| }; | |
| container.appendChild(bubble); | |
| bubbles.push(bubble); | |
| bubbleSnippets.push(null); | |
| } | |
| } | |
| function playBubbleSound(bubble, scheduledTime = null) { | |
| const index = parseInt(bubble.dataset.index); | |
| bubble.classList.add('active'); | |
| setTimeout(() => bubble.classList.remove('active'), 300); | |
| if (audioContext.state === 'suspended') audioContext.resume(); | |
| if (bubbleSnippets[index]) { | |
| const snippet = bubbleSnippets[index]; | |
| const source = audioContext.createBufferSource(); | |
| source.buffer = snippet.audioBuffer; | |
| const gain = audioContext.createGain(); | |
| gain.gain.value = 0.7; | |
| source.connect(gain); | |
| gain.connect(audioContext.destination); | |
| source.start(scheduledTime || audioContext.currentTime, snippet.startTime, snippet.duration); | |
| return; | |
| } | |
| const osc = audioContext.createOscillator(); | |
| const gain = audioContext.createGain(); | |
| osc.type = bubble.dataset.waveform; | |
| osc.frequency.setValueAtTime(bubble.dataset.frequency, audioContext.currentTime); | |
| gain.gain.setValueAtTime(0.3, audioContext.currentTime); | |
| gain.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + parseFloat(bubble.dataset.duration)); | |
| osc.connect(gain); | |
| gain.connect(audioContext.destination); | |
| const start = scheduledTime || audioContext.currentTime; | |
| osc.start(start); | |
| osc.stop(start + parseFloat(bubble.dataset.duration)); | |
| } | |
| function playRecordedSequence() { | |
| if (recordedEvents.length === 0) return; | |
| isPlaying = true; | |
| document.getElementById('playBtn').textContent = "Playing..."; | |
| const tempoFactor = 120 / currentTempo; | |
| const sequenceDuration = recordedEvents.reduce((max, e) => Math.max(max, e.time), 0) * tempoFactor + 0.5; | |
| recordedEvents.forEach(event => { | |
| const scheduledTime = audioContext.currentTime + (event.time * tempoFactor); | |
| if (bubbles[event.bubbleIndex]) { | |
| setTimeout(() => { | |
| if(isPlaying) playBubbleSound(bubbles[event.bubbleIndex]); | |
| }, event.time * tempoFactor * 1000); | |
| } | |
| }); | |
| if (isLooping) { | |
| playbackTimeout = setTimeout(playRecordedSequence, sequenceDuration * 1000); | |
| } else { | |
| playbackTimeout = setTimeout(stopPlayback, sequenceDuration * 1000); | |
| } | |
| } | |
| function stopPlayback() { | |
| isPlaying = false; | |
| if (playbackTimeout) clearTimeout(playbackTimeout); | |
| document.getElementById('playBtn').textContent = "Play"; | |
| } | |
| document.getElementById('playBtn').onclick = () => { if (!isPlaying) playRecordedSequence(); }; | |
| document.getElementById('stopBtn').onclick = stopPlayback; | |
| document.getElementById('loopBtn').onclick = function() { | |
| isLooping = !isLooping; | |
| this.classList.toggle('active', isLooping); | |
| }; | |
| document.getElementById('recordBtn').onclick = function() { | |
| isRecording = !isRecording; | |
| if (isRecording) { | |
| recordedEvents = []; | |
| recordStartTime = audioContext.currentTime; | |
| this.classList.add('active'); | |
| this.textContent = "REC..."; | |
| } else { | |
| this.classList.remove('active'); | |
| this.textContent = "Record"; | |
| } | |
| }; | |
| document.getElementById('structureBtn').onclick = function() { | |
| const container = document.getElementById('bubblesCtn'); | |
| if (currentLayout === 'circle') { | |
| const cols = 12; | |
| bubbles.forEach((b, i) => { | |
| b.style.left = `${(i % cols) * 35 + 20}px`; | |
| b.style.top = `${Math.floor(i / cols) * 35 + 20}px`; | |
| }); | |
| currentLayout = 'grid'; | |
| } else { | |
| initBubbles(); | |
| currentLayout = 'circle'; | |
| } | |
| }; | |
| document.getElementById('tempoSlider').oninput = function() { | |
| currentTempo = this.value; | |
| document.getElementById('tempoValue').textContent = this.value; | |
| }; | |
| document.getElementById('mp3UploadInput').onchange = function(e) { | |
| const files = e.target.files; | |
| for (let file of files) { | |
| const reader = new FileReader(); | |
| reader.onload = (ev) => { | |
| audioContext.decodeAudioData(ev.target.result, (buffer) => { | |
| const song = { audioBuffer: buffer, color: `hsl(${Math.random()*360}, 70%, 60%)`, name: file.name }; | |
| uploadedSongs.push(song); | |
| const item = document.createElement('div'); | |
| item.className = 'song-item'; | |
| item.innerHTML = `<div class="song-color" style="background:${song.color}"></div>${song.name.substring(0,8)}`; | |
| document.getElementById('uploadedSongsCtn').appendChild(item); | |
| assignSnippets(); | |
| }); | |
| }; | |
| reader.readAsArrayBuffer(file); | |
| } | |
| }; | |
| function assignSnippets() { | |
| for (let i = 0; i < 15; i++) { | |
| const idx = Math.floor(Math.random() * bubbles.length); | |
| const song = uploadedSongs[Math.floor(Math.random() * uploadedSongs.length)]; | |
| bubbleSnippets[idx] = { | |
| audioBuffer: song.audioBuffer, | |
| startTime: Math.random() * (song.audioBuffer.duration - 1), | |
| duration: 0.2 + Math.random() * 0.5 | |
| }; | |
| bubbles[idx].style.background = song.color; | |
| bubbles[idx].style.borderColor = "white"; | |
| } | |
| } | |
| document.getElementById('resetBubblesBtn').onclick = () => { location.reload(); }; | |
| </script> | |
| </body> | |
| </html> | |