Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>8-Bit Game Music Player</title> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.49/Tone.js"></script> | |
<style> | |
* { | |
margin: 0; | |
padding: 0; | |
box-sizing: border-box; | |
} | |
body { | |
background: #000; | |
color: #fff; | |
font-family: 'Courier New', monospace; | |
min-height: 100vh; | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
background-image: radial-gradient(#0f0 1px, transparent 1px); | |
background-size: 30px 30px; | |
} | |
.player { | |
background: rgba(0, 20, 0, 0.95); | |
border: 2px solid #0f0; | |
padding: 2rem; | |
border-radius: 15px; | |
width: 90%; | |
max-width: 400px; | |
box-shadow: 0 0 30px rgba(0, 255, 0, 0.2); | |
} | |
.title { | |
text-align: center; | |
color: #0f0; | |
text-shadow: 0 0 10px #0f0; | |
margin-bottom: 1.5rem; | |
font-size: 1.5rem; | |
} | |
.time-display { | |
text-align: center; | |
font-size: 1.2rem; | |
color: #0f0; | |
margin: 1rem 0; | |
font-family: 'Digital-7', monospace; | |
} | |
.controls { | |
display: flex; | |
justify-content: center; | |
gap: 1rem; | |
margin: 1rem 0; | |
} | |
.btn { | |
background: none; | |
border: 2px solid #0f0; | |
color: #0f0; | |
padding: 0.5rem 1rem; | |
cursor: pointer; | |
border-radius: 5px; | |
font-family: inherit; | |
transition: all 0.3s; | |
} | |
.btn:hover { | |
background: #0f0; | |
color: #000; | |
box-shadow: 0 0 15px #0f0; | |
} | |
.volume-control { | |
margin: 1.5rem 0; | |
} | |
.volume-slider { | |
width: 100%; | |
-webkit-appearance: none; | |
height: 5px; | |
background: #0f0; | |
border-radius: 5px; | |
opacity: 0.7; | |
} | |
.volume-slider::-webkit-slider-thumb { | |
-webkit-appearance: none; | |
width: 15px; | |
height: 15px; | |
background: #0f0; | |
border-radius: 50%; | |
cursor: pointer; | |
box-shadow: 0 0 10px #0f0; | |
} | |
.track-list { | |
margin-top: 1.5rem; | |
} | |
.track { | |
background: rgba(0, 50, 0, 0.3); | |
margin: 0.5rem 0; | |
padding: 1rem; | |
border: 1px solid #0f0; | |
border-radius: 8px; | |
cursor: pointer; | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
transition: all 0.3s; | |
} | |
.track:hover { | |
background: rgba(0, 100, 0, 0.3); | |
transform: translateX(5px); | |
} | |
.track.playing { | |
background: rgba(0, 255, 0, 0.2); | |
border-color: #0f0; | |
box-shadow: 0 0 15px rgba(0, 255, 0, 0.3); | |
animation: pulse 2s infinite; | |
} | |
.track-info { | |
flex: 1; | |
} | |
.track-title { | |
font-weight: bold; | |
color: #0f0; | |
} | |
.track-duration { | |
font-size: 0.8em; | |
color: #0a0; | |
} | |
.status-indicator { | |
width: 12px; | |
height: 12px; | |
border-radius: 50%; | |
margin-left: 1rem; | |
background: transparent; | |
border: 2px solid #0f0; | |
transition: all 0.3s; | |
} | |
.playing .status-indicator { | |
background: #0f0; | |
box-shadow: 0 0 10px #0f0; | |
} | |
@keyframes pulse { | |
0% { border-color: rgba(0, 255, 0, 0.5); } | |
50% { border-color: rgba(0, 255, 0, 1); } | |
100% { border-color: rgba(0, 255, 0, 0.5); } | |
} | |
@media (max-width: 480px) { | |
.player { | |
width: 95%; | |
padding: 1rem; | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<div class="player"> | |
<h1 class="title">8-BIT GAME MUSIC</h1> | |
<div class="time-display"> | |
<span id="current-time">00:00</span> / <span id="total-time">00:00</span> | |
</div> | |
<div class="controls"> | |
<button class="btn" id="play-pause">PLAY</button> | |
<button class="btn" id="stop">STOP</button> | |
</div> | |
<div class="volume-control"> | |
<input type="range" class="volume-slider" min="0" max="1" step="0.01" value="0.7"> | |
</div> | |
<div class="track-list"> | |
<div class="track" data-song="space"> | |
<div class="track-info"> | |
<div class="track-title">SPACE ADVENTURE</div> | |
<div class="track-duration">1:30</div> | |
</div> | |
<div class="status-indicator"></div> | |
</div> | |
<div class="track" data-song="battle"> | |
<div class="track-info"> | |
<div class="track-title">BATTLE ZONE</div> | |
<div class="track-duration">1:45</div> | |
</div> | |
<div class="status-indicator"></div> | |
</div> | |
<div class="track" data-song="tetris"> | |
<div class="track-info"> | |
<div class="track-title">TETRIS THEME</div> | |
<div class="track-duration">2:00</div> | |
</div> | |
<div class="status-indicator"></div> | |
</div> | |
</div> | |
</div> | |
<script> | |
let currentTime = 0; | |
let isPlaying = false; | |
let currentTrack = null; | |
let currentTrackElement = null; | |
const synth = new Tone.PolySynth(Tone.Synth, { | |
oscillator: { type: "square8" }, | |
envelope: { | |
attack: 0.02, | |
decay: 0.1, | |
sustain: 0.3, | |
release: 0.1 | |
} | |
}).connect(new Tone.Volume(-12).toDestination()); | |
// 우주 테마 | |
const spaceSequence = [ | |
["E4", "8n"], ["G4", "8n"], ["C5", "4n"], ["G4", "8n"], | |
["C5", "8n"], ["E5", "4n"], ["C5", "8n"], ["G4", "8n"], | |
["E4", "8n"], ["C4", "4n"], ["G4", "8n"], ["E4", "8n"], | |
["C4", "2n"] | |
]; | |
// 전쟁 테마 | |
const battleSequence = [ | |
["C4", "16n"], ["C4", "16n"], ["G4", "8n"], ["E4", "16n"], | |
["F4", "16n"], ["G4", "8n"], ["C4", "8n"], ["G3", "8n"], | |
["C4", "4n"], ["E4", "8n"], ["D4", "8n"], ["C4", "2n"] | |
]; | |
// 테트리스 테마 | |
const tetrisSequence = [ | |
["E5", "4n"], ["B4", "8n"], ["C5", "8n"], ["D5", "4n"], | |
["C5", "8n"], ["B4", "8n"], ["A4", "4n"], ["A4", "8n"], | |
["C5", "8n"], ["E5", "4n"], ["D5", "8n"], ["C5", "8n"], | |
["B4", "4n"], ["C5", "8n"], ["D5", "4n"], ["E5", "4n"], | |
["C5", "4n"], ["A4", "4n"], ["A4", "2n"] | |
]; | |
function updateTimer() { | |
if (isPlaying) { | |
currentTime += 0.1; | |
document.getElementById('current-time').textContent = | |
formatTime(currentTime); | |
setTimeout(updateTimer, 100); | |
} | |
} | |
function formatTime(time) { | |
const minutes = Math.floor(time / 60); | |
const seconds = Math.floor(time % 60); | |
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; | |
} | |
function stopCurrentTrack() { | |
if (currentTrack) { | |
currentTrack.stop(); | |
currentTrack.dispose(); | |
} | |
if (currentTrackElement) { | |
currentTrackElement.classList.remove('playing'); | |
} | |
isPlaying = false; | |
currentTime = 0; | |
document.getElementById('current-time').textContent = '00:00'; | |
document.getElementById('play-pause').textContent = 'PLAY'; | |
} | |
function createAndPlaySequence(sequence, element) { | |
stopCurrentTrack(); | |
currentTrack = new Tone.Part((time, note) => { | |
synth.triggerAttackRelease(note[0], note[1], time); | |
}, sequence.map((note, i) => [i * 0.25, note])); | |
currentTrack.loop = true; | |
currentTrack.start(0); | |
Tone.Transport.start(); | |
currentTrackElement = element; | |
element.classList.add('playing'); | |
isPlaying = true; | |
document.getElementById('play-pause').textContent = 'PAUSE'; | |
updateTimer(); | |
} | |
document.getElementById('play-pause').addEventListener('click', () => { | |
if (!currentTrack) return; | |
if (isPlaying) { | |
Tone.Transport.pause(); | |
document.getElementById('play-pause').textContent = 'PLAY'; | |
} else { | |
Tone.Transport.start(); | |
document.getElementById('play-pause').textContent = 'PAUSE'; | |
updateTimer(); | |
} | |
isPlaying = !isPlaying; | |
}); | |
document.getElementById('stop').addEventListener('click', stopCurrentTrack); | |
document.querySelector('.volume-slider').addEventListener('input', (e) => { | |
synth.volume.value = Tone.gainToDb(parseFloat(e.target.value)); | |
}); | |
document.querySelectorAll('.track').forEach(track => { | |
track.addEventListener('click', async () => { | |
await Tone.start(); | |
const songType = track.dataset.song; | |
if (currentTrackElement === track && isPlaying) { | |
stopCurrentTrack(); | |
return; | |
} | |
switch(songType) { | |
case 'space': | |
document.getElementById('total-time').textContent = '01:30'; | |
createAndPlaySequence(spaceSequence, track); | |
break; | |
case 'battle': | |
document.getElementById('total-time').textContent = '01:45'; | |
createAndPlaySequence(battleSequence, track); | |
break; | |
case 'tetris': | |
document.getElementById('total-time').textContent = '02:00'; | |
createAndPlaySequence(tetrisSequence, track); | |
break; | |
} | |
}); | |
}); | |
</script> | |
</body> | |
</html><script async data-explicit-opt-in="true" data-cookie-opt-in="true" src="https://vercel.live/_next-live/feedback/feedback.js"></script> |