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> |