feat: added zoom-in/out feature for piano roll window.

This commit is contained in:
Xiaohan-Tian
2026-05-01 23:26:39 -07:00
parent d59ad68f72
commit 811b3a0936
3 changed files with 85 additions and 0 deletions
+28
View File
@@ -315,3 +315,31 @@
justify-content: center;
z-index: 100;
}
.piano-roll-zoom-popup {
position: absolute;
top: 100%;
right: 0;
background-color: #2d2d2d;
border: 1px solid #444;
border-radius: 3px;
padding: 8px 12px;
z-index: 1500;
display: flex;
align-items: center;
gap: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
white-space: nowrap;
margin-top: 2px;
}
.piano-roll-zoom-popup input[type="range"] {
width: 120px;
}
.piano-roll-zoom-value {
font-size: 12px;
color: #e0e0e0;
min-width: 20px;
text-align: center;
}
+16
View File
@@ -50,6 +50,9 @@ const PianoRoll: React.FC<PianoRollProps> = ({
const [spectrogramThresholdDb, setSpectrogramThresholdDb] = useState<number>(-25);
const [spectrogramPower, setSpectrogramPower] = useState<number>(0.5);
// Piano roll zoom (1x8x); updates --region-grid-beat-width CSS variable
const [pianoRollZoom, setPianoRollZoom] = useState<number>(1);
// Quantization state
const [quantPosition, setQuantPosition] = useState<string>('1/8');
const [quantLength, setQuantLength] = useState<string>('1/8');
@@ -686,6 +689,17 @@ const PianoRoll: React.FC<PianoRollProps> = ({
container.scrollLeft = clampedScrollLeft;
}, [playheadPosition, isPlaying, autoScrollEnabled]);
// Update --region-grid-beat-width when zoom changes; reset on unmount
useEffect(() => {
document.documentElement.style.setProperty('--region-grid-beat-width', `${40 * pianoRollZoom}px`);
if (triggerNoteUpdateRef.current) {
triggerNoteUpdateRef.current(prev => prev + 1);
}
return () => {
document.documentElement.style.setProperty('--region-grid-beat-width', '40px');
};
}, [pianoRollZoom]);
// Scroll horizontally to the active region's starting bar
useEffect(() => {
if (pianoRollContentRef.current && activeRegion) {
@@ -927,6 +941,8 @@ const PianoRoll: React.FC<PianoRollProps> = ({
onThresholdChange={setSpectrogramThresholdDb}
power={spectrogramPower}
onPowerChange={setSpectrogramPower}
zoom={pianoRollZoom}
onZoomChange={setPianoRollZoom}
/>
<PianoRollContent
@@ -29,6 +29,8 @@ interface PianoRollToolbarProps {
onThresholdChange?: (db: number) => void;
power?: number;
onPowerChange?: (power: number) => void;
zoom: number;
onZoomChange: (value: number) => void;
}
const PianoRollToolbar: React.FC<PianoRollToolbarProps> = ({
@@ -49,11 +51,27 @@ const PianoRollToolbar: React.FC<PianoRollToolbarProps> = ({
onThresholdChange,
power = 0.5,
onPowerChange,
zoom,
onZoomChange,
}) => {
const isSpectrogram = mode === 'spectrogram';
const showMidiControls = mode !== 'spectrogram'; // midi-edit and hybrid
const showSpecControls = mode === 'spectrogram' || mode === 'hybrid';
const [showZoomSlider, setShowZoomSlider] = React.useState(false);
const zoomSliderRef = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
if (!showZoomSlider) return;
const handleClickOutside = (e: MouseEvent) => {
if (zoomSliderRef.current && !zoomSliderRef.current.contains(e.target as Node)) {
setShowZoomSlider(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, [showZoomSlider]);
return (
<div className="piano-roll-toolbar">
{showMidiControls && (
@@ -148,6 +166,29 @@ const PianoRollToolbar: React.FC<PianoRollToolbarProps> = ({
/>
</div>
)}
<div className="quant-dropdown-container" ref={zoomSliderRef}>
<button
className="quant-button"
onClick={() => setShowZoomSlider(!showZoomSlider)}
title="Zoom"
>
{zoom}x
</button>
{showZoomSlider && (
<div className="piano-roll-zoom-popup">
<input
type="range"
min="1"
max="8"
step="1"
value={zoom}
onChange={(e) => onZoomChange(parseInt(e.target.value))}
/>
<span className="piano-roll-zoom-value">{zoom}x</span>
</div>
)}
</div>
</div>
</div>
);