Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>MS1-X MOUSE MODULATION INDEX</title> | |
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&family=Share+Tech+Mono&display=swap" rel="stylesheet"> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.49/Tone.js"></script> | |
<style> | |
body { | |
margin: 0; | |
background: radial-gradient(circle at center, #0f1419 0%, #080a0c 100%); | |
color: #39ff14; | |
font-family: 'Share Tech Mono', monospace; | |
min-height: 100vh; | |
display: flex; | |
flex-direction: column; | |
align-items: center; | |
overflow: hidden; | |
} | |
.container { | |
width: 100%; | |
max-width: 1200px; | |
padding: 20px; | |
} | |
.header { | |
text-align: center; | |
margin-bottom: 30px; | |
position: relative; | |
} | |
.header:after { | |
content: ''; | |
position: absolute; | |
bottom: -10px; | |
left: 50%; | |
transform: translateX(-50%); | |
width: 300px; | |
height: 2px; | |
background: linear-gradient(90deg, transparent, #39ff14, transparent); | |
} | |
.title { | |
font-family: 'Orbitron', sans-serif; | |
font-size: 48px; | |
margin: 0; | |
text-shadow: 0 0 20px #39ff1480; | |
letter-spacing: 5px; | |
background: linear-gradient(180deg, #39ff14, #2ba80d); | |
-webkit-background-clip: text; | |
-webkit-text-fill-color: transparent; | |
} | |
.subtitle { | |
font-size: 14px; | |
opacity: 0.8; | |
margin: 5px 0; | |
letter-spacing: 2px; | |
} | |
.xy-container { | |
position: relative; | |
width: 500px; | |
height: 500px; | |
margin: 0 auto; | |
padding: 20px; | |
background: linear-gradient(45deg, #111417, #1a1e23); | |
border-radius: 20px; | |
box-shadow: | |
inset 0 0 50px rgba(57, 255, 20, 0.1), | |
0 0 20px rgba(0,0,0,0.5); | |
} | |
.xy-pad { | |
width: 100%; | |
height: 100%; | |
background: #0a0c0e; | |
border: 2px solid #39ff14; | |
border-radius: 15px; | |
position: relative; | |
box-shadow: | |
0 0 20px #39ff1440, | |
inset 0 0 50px rgba(57, 255, 20, 0.1); | |
overflow: hidden; | |
} | |
.xy-cursor { | |
width: 16px; | |
height: 16px; | |
background: #39ff14; | |
border-radius: 50%; | |
position: absolute; | |
transform: translate(-50%, -50%); | |
pointer-events: none; | |
box-shadow: | |
0 0 20px #39ff14, | |
0 0 40px #39ff14, | |
0 0 60px #39ff14; | |
z-index: 10; | |
} | |
.grid-lines { | |
position: absolute; | |
width: 100%; | |
height: 100%; | |
background-image: | |
radial-gradient(circle, rgba(57, 255, 20, 0.1) 1px, transparent 1px), | |
linear-gradient(rgba(57, 255, 20, 0.1) 1px, transparent 1px), | |
linear-gradient(90deg, rgba(57, 255, 20, 0.1) 1px, transparent 1px); | |
background-size: | |
20px 20px, | |
50px 50px, | |
50px 50px; | |
opacity: 0.5; | |
} | |
.controls { | |
display: flex; | |
justify-content: center; | |
gap: 15px; | |
margin: 20px 0; | |
flex-wrap: wrap; | |
} | |
.toggle { | |
background: linear-gradient(180deg, #1a1e23, #141719); | |
border: 1px solid #39ff14; | |
color: #39ff14; | |
padding: 12px 24px; | |
font-family: 'Share Tech Mono', monospace; | |
cursor: pointer; | |
transition: all 0.3s; | |
border-radius: 5px; | |
text-shadow: 0 0 5px #39ff14; | |
box-shadow: | |
0 0 10px rgba(57, 255, 20, 0.2), | |
inset 0 0 20px rgba(57, 255, 20, 0.1); | |
} | |
.toggle:hover { | |
background: linear-gradient(180deg, #39ff14, #2ba80d); | |
color: #0a0c0e; | |
text-shadow: none; | |
} | |
.toggle.active { | |
background: #39ff14; | |
color: #0a0c0e; | |
box-shadow: | |
0 0 20px #39ff14, | |
inset 0 0 10px rgba(0,0,0,0.2); | |
} | |
.piano { | |
display: flex; | |
justify-content: center; | |
margin: 20px auto; | |
height: 120px; | |
max-width: 800px; | |
perspective: 1000px; | |
transform: rotateX(5deg); | |
} | |
.key { | |
width: 35px; | |
height: 100%; | |
background: linear-gradient(180deg, #1a1e23, #141719); | |
border: 1px solid #39ff14; | |
margin: 0 2px; | |
transition: all 0.1s; | |
box-shadow: | |
0 5px 15px rgba(0,0,0,0.5), | |
inset 0 0 20px rgba(57, 255, 20, 0.1); | |
} | |
.key.active { | |
background: #39ff14; | |
transform: scale(0.98); | |
box-shadow: | |
0 0 30px #39ff14, | |
inset 0 0 10px rgba(0,0,0,0.2); | |
} | |
.oscilloscope { | |
position: absolute; | |
width: 100%; | |
height: 100%; | |
z-index: 5; | |
} | |
@keyframes scanline { | |
0% { | |
transform: translateY(0); | |
} | |
100% { | |
transform: translateY(100%); | |
} | |
} | |
.scanline { | |
position: absolute; | |
width: 100%; | |
height: 2px; | |
background: rgba(57, 255, 20, 0.1); | |
animation: scanline 2s linear infinite; | |
z-index: 4; | |
pointer-events: none; | |
} | |
</style> | |
</head> | |
<body> | |
<div class="container"> | |
<div class="header"> | |
<h1 class="title">MS1-X</h1> | |
<p class="subtitle">USE QWERTY KEYBOARD TO PLAY</p> | |
<p class="subtitle">CLICK AND DRAG YOUR MOUSE TO CHANGE THE SOUND</p> | |
</div> | |
<div class="xy-container"> | |
<div class="xy-pad"> | |
<div class="grid-lines"></div> | |
<div class="scanline"></div> | |
<canvas class="oscilloscope"></canvas> | |
<div class="xy-cursor"></div> | |
</div> | |
</div> | |
<div class="controls"> | |
<button class="toggle" data-effect="bitcrusher">BITCRUSHER</button> | |
<button class="toggle" data-effect="delay">DELAY</button> | |
<button class="toggle" data-effect="distortion">DISTORTION</button> | |
<button class="toggle" data-effect="chorus">CHORUS</button> | |
</div> | |
<div class="piano"></div> | |
</div> | |
<script> | |
const synth = new Tone.FMSynth({ | |
modulationIndex: 10, | |
harmonicity: 3, | |
envelope: { | |
attack: 0.01, | |
decay: 0.2, | |
sustain: 0.8, | |
release: 1 | |
} | |
}).toDestination(); | |
const delay = new Tone.FeedbackDelay({ | |
delayTime: 0.3, | |
feedback: 0.4, | |
wet: 0 | |
}); | |
const chorus = new Tone.Chorus({ | |
frequency: 4, | |
delayTime: 2.5, | |
depth: 0.5, | |
wet: 0 | |
}).start(); | |
const crusher = new Tone.BitCrusher({ | |
bits: 4, | |
wet: 0 | |
}); | |
const distortion = new Tone.Distortion({ | |
distortion: 0.8, | |
wet: 0 | |
}); | |
// Effects chain | |
synth.chain(crusher, distortion, chorus, delay, Tone.Destination); | |
// Analyzer for oscilloscope | |
const analyzer = new Tone.Analyser('waveform', 256); | |
synth.connect(analyzer); | |
const keyMap = { | |
'z': 'C2', 'x': 'D2', 'c': 'E2', 'v': 'F2', 'b': 'G2', 'n': 'A2', 'm': 'B2', | |
'a': 'C3', 's': 'D3', 'd': 'E3', 'f': 'F3', 'g': 'G3', 'h': 'A3', 'j': 'B3', | |
'q': 'C4', 'w': 'D4', 'e': 'E4', 'r': 'F4', 't': 'G4', 'y': 'A4', 'u': 'B4', | |
'i': 'C5', 'o': 'D5', 'p': 'E5', '[': 'F5', ']': 'G5', '\\': 'A5' | |
}; | |
const activeKeys = new Set(); | |
const piano = document.querySelector('.piano'); | |
// Create piano keys | |
Object.keys(keyMap).forEach(key => { | |
const keyEl = document.createElement('div'); | |
keyEl.className = 'key'; | |
keyEl.dataset.key = key; | |
piano.appendChild(keyEl); | |
}); | |
// Oscilloscope | |
const canvas = document.querySelector('.oscilloscope'); | |
const ctx = canvas.getContext('2d'); | |
function resizeCanvas() { | |
canvas.width = canvas.offsetWidth; | |
canvas.height = canvas.offsetHeight; | |
} | |
resizeCanvas(); | |
window.addEventListener('resize', resizeCanvas); | |
function drawOscilloscope() { | |
requestAnimationFrame(drawOscilloscope); | |
const values = analyzer.getValue(); | |
ctx.clearRect(0, 0, canvas.width, canvas.height); | |
ctx.beginPath(); | |
ctx.strokeStyle = '#39ff14'; | |
ctx.lineWidth = 2; | |
for(let i = 0; i < values.length; i++) { | |
const x = (i / values.length) * canvas.width; | |
const y = ((values[i] + 1) / 2) * canvas.height; | |
if(i === 0) { | |
ctx.moveTo(x, y); | |
} else { | |
ctx.lineTo(x, y); | |
} | |
} | |
ctx.stroke(); | |
ctx.strokeStyle = '#39ff1440'; | |
ctx.lineWidth = 1; | |
ctx.stroke(); | |
} | |
drawOscilloscope(); | |
// XY Pad | |
const xyPad = document.querySelector('.xy-pad'); | |
const cursor = document.querySelector('.xy-cursor'); | |
let isDrawing = false; | |
function updateXY(e) { | |
const rect = xyPad.getBoundingClientRect(); | |
const x = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)); | |
const y = Math.max(0, Math.min(1, (e.clientY - rect.top) / rect.height)); | |
cursor.style.left = `${x * rect.width}px`; | |
cursor.style.top = `${y * rect.height}px`; | |
synth.modulationIndex.value = x * 100; | |
synth.harmonicity.value = y * 10; | |
delay.delayTime.value = x; | |
delay.feedback.value = y; | |
distortion.distortion = y * 2; | |
chorus.frequency.value = x * 10; | |
} | |
xyPad.addEventListener('mousedown', (e) => { | |
isDrawing = true; | |
updateXY(e); | |
}); | |
window.addEventListener('mousemove', (e) => { | |
if (isDrawing) updateXY(e); | |
}); | |
window.addEventListener('mouseup', () => { | |
isDrawing = false; | |
}); | |
// Keyboard Events | |
window.addEventListener('keydown', (e) => { | |
const key = e.key.toLowerCase(); | |
if (keyMap[key] && !activeKeys.has(key)) { | |
synth.triggerAttack(keyMap[key]); | |
activeKeys.add(key); | |
document.querySelector(`.key[data-key="${key}"]`)?.classList.add('active'); | |
} | |
}); | |
window.addEventListener('keyup', (e) => { | |
const key = e.key.toLowerCase(); | |
if (keyMap[key]) { | |
synth.triggerRelease(); | |
activeKeys.delete(key); | |
document.querySelector(`.key[data-key="${key}"]`)?.classList.remove('active'); | |
} | |
}); | |
// Effect Toggles | |
document.querySelectorAll('.toggle').forEach(btn => { | |
btn.addEventListener('click', () => { | |
btn.classList.toggle('active'); | |
const effect = btn.dataset.effect; | |
switch(effect) { | |
case 'bitcrusher': | |
crusher.wet.value = btn.classList.contains('active') ? 1 : 0; | |
break; | |
case 'delay': | |
delay.wet.value = btn.classList.contains('active') ? 0.5 : 0; | |
break; | |
case 'distortion': | |
distortion.wet.value = btn.classList.contains('active') ? 0.5 : 0; | |
break; | |
case 'chorus': | |
chorus.wet.value = btn.classList.contains('active') ? 0.5 : 0; | |
break; | |
} | |
}); | |
}); | |
// Start audio context on first interaction | |
document.body.addEventListener('click', () => { | |
Tone.start(); | |
}, { once: true }); | |
</script> | |
</body> | |
</html> |