mirror of
https://github.com/9x/sheetmusictrainer.git
synced 2026-11-03 22:03:54 +01:00
Metronome always-visible controls, Giuliani patterns, pool fix
- Metronome widget: BPM/slider row and pendulum always visible (dimmed when off); the toggle gates sound + animation only. BPM/Timer mode switch restored for single-note auto-advance. - Giuliani right-hand figures as arpeggio patterns: p-i-m, p-i-m-a, p-a-m-i, m-a-m-i, a-m-i — cycle [root, third, fifth, octave] per figure with octave register shift per repetition (120-studies feel). - Pool fix: fretted instruments without active filters now get the full 0-12 fretboard in ALL modes (legacy difficulty ranges no longer clamp the pool since the Note Set dropdown was removed). - Tests 174/174.
This commit is contained in:
@@ -87,76 +87,87 @@ export const MetronomeWidget: React.FC<MetronomeWidgetProps> = ({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{rhythm.active && (
|
||||
<div className="rhythm-details" style={{ display: 'flex', flexDirection: 'column', gap: '6px', paddingTop: '8px', borderTop: '1px solid rgba(255,255,255,0.1)' }}>
|
||||
{/* Linear pendulum — same element, left transitions each beat */}
|
||||
{/* Always visible: pendulum + BPM; the toggle gates sound + animation */}
|
||||
<div className="rhythm-details" style={{ display: 'flex', flexDirection: 'column', gap: '6px', paddingTop: '8px', borderTop: '1px solid rgba(255,255,255,0.1)', opacity: rhythm.active ? 1 : 0.5 }}>
|
||||
{/* Linear pendulum — same element, left transitions each beat */}
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
height: compact ? 14 : 18,
|
||||
borderRadius: 9,
|
||||
background: 'color-mix(in srgb, var(--color-text-main) 8%, transparent)',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
aria-hidden
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
height: compact ? 14 : 18,
|
||||
borderRadius: 9,
|
||||
background: 'color-mix(in srgb, var(--color-text-main) 8%, transparent)',
|
||||
overflow: 'hidden',
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: beatParity === 0 ? '6%' : '88%',
|
||||
width: compact ? 10 : 12,
|
||||
height: compact ? 10 : 12,
|
||||
borderRadius: '50%',
|
||||
background: 'var(--color-text-main)',
|
||||
transform: 'translateY(-50%)',
|
||||
transition: `left ${beatMs}ms linear`,
|
||||
display: rhythm.active ? 'block' : 'none',
|
||||
}}
|
||||
aria-hidden
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: beatParity === 0 ? '6%' : '88%',
|
||||
width: compact ? 10 : 12,
|
||||
height: compact ? 10 : 12,
|
||||
borderRadius: '50%',
|
||||
background: 'var(--color-text-main)',
|
||||
transform: 'translateY(-50%)',
|
||||
transition: `left ${beatMs}ms linear`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
/>
|
||||
</div>
|
||||
|
||||
{rhythm.mode === 'bpm' || compact ? (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||||
<button
|
||||
className="control-button small"
|
||||
onClick={() => onUpdate({ bpm: Math.max(30, rhythm.bpm - 1) })}
|
||||
style={{ width: '24px', padding: '0', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
|
||||
>
|
||||
−
|
||||
</button>
|
||||
<input
|
||||
type="number"
|
||||
min="30"
|
||||
max="300"
|
||||
value={rhythm.bpm}
|
||||
onChange={(e) => onUpdate({ bpm: Math.max(1, parseInt(e.target.value) || 60) })}
|
||||
className="control-input"
|
||||
style={{ width: '48px', textAlign: 'center', padding: '2px' }}
|
||||
/>
|
||||
<button
|
||||
className="control-button small"
|
||||
onClick={() => onUpdate({ bpm: Math.min(300, rhythm.bpm + 1) })}
|
||||
style={{ width: '24px', padding: '0', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
<input
|
||||
type="range"
|
||||
min="30"
|
||||
max="240"
|
||||
step="1"
|
||||
value={rhythm.bpm}
|
||||
onChange={(e) => onUpdate({ bpm: Number(e.target.value) })}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ fontSize: '11px', opacity: 0.7, textAlign: 'center' }}>
|
||||
{getTempoMarking(rhythm.bpm)}
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||||
{!compact && (
|
||||
<div style={{ display: 'flex', gap: '4px', flexShrink: 0 }}>
|
||||
<button
|
||||
className={`control-button small ${rhythm.mode === 'bpm' ? 'active' : ''}`}
|
||||
onClick={() => onUpdate({ mode: 'bpm' })}
|
||||
style={{ fontSize: '10px' }}
|
||||
>BPM</button>
|
||||
<button
|
||||
className={`control-button small ${rhythm.mode === 'seconds' ? 'active' : ''}`}
|
||||
onClick={() => onUpdate({ mode: 'seconds' })}
|
||||
style={{ fontSize: '10px' }}
|
||||
>Timer</button>
|
||||
</div>
|
||||
)}
|
||||
{rhythm.mode === 'bpm' ? (
|
||||
<>
|
||||
<button
|
||||
className="control-button small"
|
||||
onClick={() => onUpdate({ bpm: Math.max(30, rhythm.bpm - 1) })}
|
||||
style={{ width: '24px', padding: '0', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
|
||||
>
|
||||
−
|
||||
</button>
|
||||
<input
|
||||
type="number"
|
||||
min="30"
|
||||
max="300"
|
||||
value={rhythm.bpm}
|
||||
onChange={(e) => onUpdate({ bpm: Math.max(1, parseInt(e.target.value) || 60) })}
|
||||
className="control-input"
|
||||
style={{ width: '48px', textAlign: 'center', padding: '2px' }}
|
||||
/>
|
||||
<button
|
||||
className="control-button small"
|
||||
onClick={() => onUpdate({ bpm: Math.min(300, rhythm.bpm + 1) })}
|
||||
style={{ width: '24px', padding: '0', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
<input
|
||||
type="range"
|
||||
min="30"
|
||||
max="240"
|
||||
step="1"
|
||||
value={rhythm.bpm}
|
||||
onChange={(e) => onUpdate({ bpm: Number(e.target.value) })}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||||
<>
|
||||
<span style={{ fontSize: '12px', minWidth: '32px' }}>{rhythm.seconds}s</span>
|
||||
<input
|
||||
type="range"
|
||||
@@ -167,8 +178,36 @@ export const MetronomeWidget: React.FC<MetronomeWidgetProps> = ({
|
||||
onChange={(e) => onUpdate({ seconds: Number(e.target.value) })}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{rhythm.mode === 'bpm' && (
|
||||
<div style={{ fontSize: '11px', opacity: 0.7, textAlign: 'center' }}>
|
||||
{getTempoMarking(rhythm.bpm)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center' }}>
|
||||
{showAutoAdvance ? (
|
||||
<label style={{ fontSize: '10px', display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={rhythm.autoAdvance}
|
||||
onChange={(e) => onUpdate({ autoAdvance: e.target.checked })}
|
||||
/>
|
||||
Auto
|
||||
</label>
|
||||
) : <span />}
|
||||
<label style={{ fontSize: '10px', display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={rhythm.sound}
|
||||
onChange={(e) => onUpdate({ sound: e.target.checked })}
|
||||
/>
|
||||
Sound
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
{showAutoAdvance ? (
|
||||
@@ -189,9 +228,7 @@ export const MetronomeWidget: React.FC<MetronomeWidgetProps> = ({
|
||||
/>
|
||||
Sound
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user