fix: proper fullscreen piano (1 octave, big keys) + block native zoom
Fullscreen piano redesign: - 1 octave with 7 large white keys filling the entire screen - Gradient-lit keys with cyan press highlight - Octave navigation buttons (◀ ▶) to shift up/down - Note labels on each key (C4, D4, etc.) - Black keys proportionally sized at 58% height - touch-action: none to prevent any browser interference Block native browser zoom: - viewport meta: maximum-scale=1.0, user-scalable=no - html touch-action: manipulation (prevents double-tap zoom on Safari) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<title>Reaktor — MontLab Modular Synth</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
</head>
|
||||
|
||||
@@ -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 (
|
||||
<div className="keyboard-fullscreen">
|
||||
<div className="keyboard-fs-header">
|
||||
<span className="keyboard-fs-title">🎹 Piano — Oct {octave}</span>
|
||||
<button className="keyboard-fs-close" onClick={onClose}>✕</button>
|
||||
<button className="keyboard-fs-oct-btn" onClick={() => setOct(o => Math.max(1, o - 1))}>◀</button>
|
||||
<span className="keyboard-fs-title">Octave {oct}</span>
|
||||
<button className="keyboard-fs-oct-btn" onClick={() => setOct(o => Math.min(8, o + 1))}>▶</button>
|
||||
<div style={{ width: 36 }} />
|
||||
</div>
|
||||
<div className="keyboard-fs-keys">
|
||||
{/* White keys */}
|
||||
{whites.map((k, i) => (
|
||||
{whiteKeys.map((k, i) => (
|
||||
<div
|
||||
key={`w${i}`}
|
||||
className="keyboard-fs-white"
|
||||
style={{ width: `${keyW}%` }}
|
||||
onPointerDown={() => 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)}
|
||||
>
|
||||
<span className="keyboard-fs-note-label">
|
||||
{NOTE_NAMES[k.note % 12]}
|
||||
</span>
|
||||
<span className="keyboard-fs-note-label">{k.name}{oct}</span>
|
||||
</div>
|
||||
))}
|
||||
{/* Black keys */}
|
||||
{blackPositions.map((bp, i) => (
|
||||
{blackKeys.map((k) => (
|
||||
<div
|
||||
key={`b${i}`}
|
||||
className="keyboard-fs-black"
|
||||
style={{ left: `${(bp.whiteIdx + 0.65) * keyW}%`, width: `${keyW * 0.7}%` }}
|
||||
onPointerDown={() => 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)}
|
||||
>
|
||||
<span className="keyboard-fs-black-label">{k.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -188,7 +184,7 @@ export default function KeyboardWidget({ moduleId }) {
|
||||
{fullscreen && (
|
||||
<FullscreenPiano
|
||||
moduleId={moduleId}
|
||||
octave={octave}
|
||||
initialOctave={octave}
|
||||
onClose={() => setFullscreen(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user