diff --git a/index.html b/index.html index 903f633..10f01cf 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,7 @@ - + Reaktor — MontLab Modular Synth diff --git a/src/components/KeyboardWidget.jsx b/src/components/KeyboardWidget.jsx index ac98edd..99dcbda 100644 --- a/src/components/KeyboardWidget.jsx +++ b/src/components/KeyboardWidget.jsx @@ -16,84 +16,80 @@ function midiToFreq(midi) { return 440 * Math.pow(2, (midi - 69) / 12); } -// Fullscreen piano overlay for mobile -function FullscreenPiano({ moduleId, octave, onClose }) { - const activeRef = useRef(new Set()); +// Fullscreen piano — 1 octave, big comfortable keys like a real piano app +function FullscreenPiano({ moduleId, initialOctave, onClose }) { + const [oct, setOct] = useState(initialOctave); + const [activeNotes, setActiveNotes] = useState(new Set()); - const playNote = useCallback((semitone) => { - const midi = (octave + 1) * 12 + semitone; + const play = useCallback((semitone) => { + const midi = (oct + 1) * 12 + semitone; triggerKeyboard(moduleId, midiToFreq(midi), true); - }, [moduleId, octave]); + setActiveNotes(prev => new Set(prev).add(semitone)); + }, [moduleId, oct]); - const stopNote = useCallback(() => { - triggerKeyboard(moduleId, 440, false); + const stop = useCallback((semitone) => { + setActiveNotes(prev => { + const next = new Set(prev); + next.delete(semitone); + if (next.size === 0) triggerKeyboard(moduleId, 440, false); + return next; + }); }, [moduleId]); - const handleTouch = useCallback((semitone, isDown) => { - if (isDown) { - activeRef.current.add(semitone); - playNote(semitone); - } else { - activeRef.current.delete(semitone); - if (activeRef.current.size === 0) stopNote(); - } - }, [playNote, stopNote]); - - // 2 octaves of keys - const whites = []; - const blacks = []; - const whiteNotes = [0, 2, 4, 5, 7, 9, 11]; - const blackNotes = [1, 3, -1, 6, 8, 10, -1]; - - for (let oct = 0; oct < 2; oct++) { - const offset = oct * 12; - whiteNotes.forEach((note, i) => { - whites.push({ note: note + offset, idx: whites.length }); - }); - } - - const blackPositions = [ - { note: 1, whiteIdx: 0 }, { note: 3, whiteIdx: 1 }, - { note: 6, whiteIdx: 3 }, { note: 8, whiteIdx: 4 }, { note: 10, whiteIdx: 5 }, - { note: 13, whiteIdx: 7 }, { note: 15, whiteIdx: 8 }, - { note: 18, whiteIdx: 10 }, { note: 20, whiteIdx: 11 }, { note: 22, whiteIdx: 12 }, + // 1 octave: 7 white keys, 5 black keys + const whiteKeys = [ + { note: 0, name: 'C' }, + { note: 2, name: 'D' }, + { note: 4, name: 'E' }, + { note: 5, name: 'F' }, + { note: 7, name: 'G' }, + { note: 9, name: 'A' }, + { note: 11, name: 'B' }, ]; - const totalWhites = whites.length; - const keyW = 100 / totalWhites; + // Black key positions relative to white key index (0-6) + const blackKeys = [ + { note: 1, name: 'C#', after: 0 }, + { note: 3, name: 'D#', after: 1 }, + { note: 6, name: 'F#', after: 3 }, + { note: 8, name: 'G#', after: 4 }, + { note: 10, name: 'A#', after: 5 }, + ]; return (
- 🎹 Piano — Oct {octave} + + Octave {oct} + +
- {/* White keys */} - {whites.map((k, i) => ( + {whiteKeys.map((k, i) => (
handleTouch(k.note, true)} - onPointerUp={() => handleTouch(k.note, false)} - onPointerLeave={() => handleTouch(k.note, false)} + key={k.note} + className={`keyboard-fs-white ${activeNotes.has(k.note) ? 'pressed' : ''}`} + onPointerDown={() => play(k.note)} + onPointerUp={() => stop(k.note)} + onPointerLeave={() => stop(k.note)} + onPointerCancel={() => stop(k.note)} > - - {NOTE_NAMES[k.note % 12]} - + {k.name}{oct}
))} - {/* Black keys */} - {blackPositions.map((bp, i) => ( + {blackKeys.map((k) => (
handleTouch(bp.note, true)} - onPointerUp={() => handleTouch(bp.note, false)} - onPointerLeave={() => handleTouch(bp.note, false)} - /> + key={k.note} + className={`keyboard-fs-black ${activeNotes.has(k.note) ? 'pressed' : ''}`} + style={{ left: `${(k.after + 0.65) * (100 / 7)}%`, width: `${(100 / 7) * 0.65}%` }} + onPointerDown={() => play(k.note)} + onPointerUp={() => stop(k.note)} + onPointerLeave={() => stop(k.note)} + onPointerCancel={() => stop(k.note)} + > + {k.name} +
))}
@@ -188,7 +184,7 @@ export default function KeyboardWidget({ moduleId }) { {fullscreen && ( setFullscreen(false)} /> )} diff --git a/src/index.css b/src/index.css index 87ebc40..5e4f1dc 100644 --- a/src/index.css +++ b/src/index.css @@ -30,7 +30,11 @@ html, body, #root { font-family: 'Inter', 'SF Pro', -apple-system, system-ui, sans-serif; font-size: 13px; -webkit-font-smoothing: antialiased; + touch-action: pan-x pan-y; + -ms-touch-action: pan-x pan-y; } +/* Block native browser zoom gestures globally */ +html { touch-action: manipulation; } /* ===== Layout ===== */ .app { display: flex; flex-direction: column; height: 100vh; } @@ -790,46 +794,72 @@ html, body, #root { .keyboard-fullscreen { position: fixed; inset: 0; z-index: 500; background: #050510; display: flex; flex-direction: column; - animation: fadeIn 0.2s ease-out; + animation: fadeIn 0.2s ease-out; touch-action: none; } @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } .keyboard-fs-header { - display: flex; align-items: center; justify-content: space-between; - padding: 12px 16px; background: var(--panel); border-bottom: 1px solid var(--border); + display: flex; align-items: center; justify-content: center; + gap: 16px; padding: 8px 16px; background: var(--panel); + border-bottom: 1px solid var(--border); flex-shrink: 0; +} +.keyboard-fs-title { + font-size: 16px; font-weight: 700; color: var(--text); + font-family: 'JetBrains Mono', monospace; letter-spacing: 1px; } -.keyboard-fs-title { font-size: 14px; font-weight: 600; color: var(--text); } .keyboard-fs-close { width: 36px; height: 36px; border-radius: 8px; background: var(--surface); border: 1px solid var(--border); color: var(--text); font-size: 16px; cursor: pointer; display: flex; align-items: center; justify-content: center; } +.keyboard-fs-oct-btn { + width: 44px; height: 36px; border-radius: 8px; + background: var(--surface); border: 1px solid var(--border); + color: var(--accent); font-size: 16px; cursor: pointer; + display: flex; align-items: center; justify-content: center; +} +.keyboard-fs-oct-btn:active { background: var(--accent); color: #000; } .keyboard-fs-keys { flex: 1; position: relative; display: flex; - padding: 0; touch-action: none; user-select: none; + touch-action: none; user-select: none; } .keyboard-fs-white { - flex-shrink: 0; height: 100%; background: #1a1a2e; - border-right: 1px solid #252545; position: relative; + flex: 1; height: 100%; background: linear-gradient(to bottom, #1e1e38, #14142a); + border-right: 2px solid #0a0a18; position: relative; display: flex; align-items: flex-end; justify-content: center; - padding-bottom: 16px; cursor: pointer; transition: background 0.05s; + padding-bottom: 20px; cursor: pointer; transition: background 0.05s; +} +.keyboard-fs-white.pressed, .keyboard-fs-white:active { + background: linear-gradient(to bottom, var(--accent), #0a8a9e); } -.keyboard-fs-white:active { background: var(--accent); } .keyboard-fs-note-label { - font-size: 10px; color: var(--text2); font-family: 'JetBrains Mono', monospace; - pointer-events: none; + font-size: 14px; font-weight: 600; color: var(--text2); + font-family: 'JetBrains Mono', monospace; pointer-events: none; } +.keyboard-fs-white.pressed .keyboard-fs-note-label, .keyboard-fs-white:active .keyboard-fs-note-label { color: #000; } .keyboard-fs-black { - position: absolute; top: 0; height: 55%; - background: #0a0a18; border: 1px solid #333; - border-top: none; border-radius: 0 0 4px 4px; + position: absolute; top: 0; height: 58%; + background: linear-gradient(to bottom, #0a0a16, #060610); + border: 2px solid #222; border-top: none; + border-radius: 0 0 6px 6px; z-index: 2; cursor: pointer; transition: background 0.05s; + display: flex; align-items: flex-end; justify-content: center; + padding-bottom: 8px; } -.keyboard-fs-black:active { background: var(--accent); } +.keyboard-fs-black.pressed, .keyboard-fs-black:active { + background: linear-gradient(to bottom, #0088aa, #006688); + border-color: var(--accent); +} +.keyboard-fs-black-label { + font-size: 10px; font-weight: 600; color: #555; + font-family: 'JetBrains Mono', monospace; pointer-events: none; +} +.keyboard-fs-black.pressed .keyboard-fs-black-label, +.keyboard-fs-black:active .keyboard-fs-black-label { color: var(--accent); } /* ===== Drum Pad ===== */ .drumpad-grid {