feat: added auto-center playhead to piano roll window
This commit is contained in:
@@ -33,6 +33,8 @@ const MainContent: React.FC<MainContentProps> = ({
|
|||||||
setPlayheadPosition,
|
setPlayheadPosition,
|
||||||
playheadPosition,
|
playheadPosition,
|
||||||
isPlaying,
|
isPlaying,
|
||||||
|
autoScrollEnabled,
|
||||||
|
setAutoScrollEnabled,
|
||||||
clearAllSelections,
|
clearAllSelections,
|
||||||
setSelectedTrack,
|
setSelectedTrack,
|
||||||
showPianoRoll,
|
showPianoRoll,
|
||||||
@@ -84,7 +86,6 @@ const MainContent: React.FC<MainContentProps> = ({
|
|||||||
|
|
||||||
// Refs for auto-scroll during playback
|
// Refs for auto-scroll during playback
|
||||||
const mainContentRef = useRef<HTMLDivElement | null>(null);
|
const mainContentRef = useRef<HTMLDivElement | null>(null);
|
||||||
const userManuallyScrolledRef = useRef(false);
|
|
||||||
const expectedScrollLeftRef = useRef<number>(-1);
|
const expectedScrollLeftRef = useRef<number>(-1);
|
||||||
const isPlayingRef = useRef(false);
|
const isPlayingRef = useRef(false);
|
||||||
|
|
||||||
@@ -95,11 +96,8 @@ const MainContent: React.FC<MainContentProps> = ({
|
|||||||
const loopDragStartXRef = useRef<number | null>(null);
|
const loopDragStartXRef = useRef<number | null>(null);
|
||||||
const loopDragOriginalSettingsRef = useRef<{ isLooping: boolean; loopingRange: [number, number] } | null>(null);
|
const loopDragOriginalSettingsRef = useRef<{ isLooping: boolean; loopingRange: [number, number] } | null>(null);
|
||||||
|
|
||||||
// Sync isPlayingRef and reset manual-scroll flag when playback starts
|
// Sync isPlayingRef for use inside scroll event closure
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isPlaying && !isPlayingRef.current) {
|
|
||||||
userManuallyScrolledRef.current = false;
|
|
||||||
}
|
|
||||||
isPlayingRef.current = isPlaying;
|
isPlayingRef.current = isPlaying;
|
||||||
}, [isPlaying]);
|
}, [isPlaying]);
|
||||||
|
|
||||||
@@ -110,9 +108,8 @@ const MainContent: React.FC<MainContentProps> = ({
|
|||||||
|
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
if (!isPlayingRef.current) return;
|
if (!isPlayingRef.current) return;
|
||||||
// If scrollLeft matches what we programmatically set (within 1px), it's our scroll — ignore
|
|
||||||
if (Math.abs(container.scrollLeft - expectedScrollLeftRef.current) < 1) return;
|
if (Math.abs(container.scrollLeft - expectedScrollLeftRef.current) < 1) return;
|
||||||
userManuallyScrolledRef.current = true;
|
useProjectStore.getState().setAutoScrollEnabled(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
container.addEventListener('scroll', handleScroll);
|
container.addEventListener('scroll', handleScroll);
|
||||||
@@ -121,7 +118,7 @@ const MainContent: React.FC<MainContentProps> = ({
|
|||||||
|
|
||||||
// Auto-scroll to keep playhead centered during playback
|
// Auto-scroll to keep playhead centered during playback
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isPlaying || userManuallyScrolledRef.current) return;
|
if (!isPlaying || !autoScrollEnabled) return;
|
||||||
|
|
||||||
const container = mainContentRef.current;
|
const container = mainContentRef.current;
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
@@ -143,7 +140,7 @@ const MainContent: React.FC<MainContentProps> = ({
|
|||||||
|
|
||||||
expectedScrollLeftRef.current = clampedScrollLeft;
|
expectedScrollLeftRef.current = clampedScrollLeft;
|
||||||
container.scrollLeft = clampedScrollLeft;
|
container.scrollLeft = clampedScrollLeft;
|
||||||
}, [playheadPosition, isPlaying, timeSignature]);
|
}, [playheadPosition, isPlaying, autoScrollEnabled, timeSignature]);
|
||||||
|
|
||||||
// Effect to verify track updates
|
// Effect to verify track updates
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ const PianoRoll: React.FC<PianoRollProps> = ({
|
|||||||
initialPosition,
|
initialPosition,
|
||||||
initialSize
|
initialSize
|
||||||
}) => {
|
}) => {
|
||||||
const { maxBars, tracks, updateTrack, timeSignature, showChatBox, showInstrumentSelection, keySignature, selectedMode, setSelectedMode } = useProjectStore();
|
const { maxBars, tracks, updateTrack, timeSignature, showChatBox, showInstrumentSelection, keySignature, selectedMode, setSelectedMode, playheadPosition, isPlaying, autoScrollEnabled } = useProjectStore();
|
||||||
|
|
||||||
// Tool state for piano roll
|
// Tool state for piano roll
|
||||||
const [activeTool, setActiveTool] = useState<'pointer' | 'pencil'>('pointer');
|
const [activeTool, setActiveTool] = useState<'pointer' | 'pencil'>('pointer');
|
||||||
@@ -59,6 +59,10 @@ const PianoRoll: React.FC<PianoRollProps> = ({
|
|||||||
const pianoGridRef = useRef<HTMLDivElement>(null);
|
const pianoGridRef = useRef<HTMLDivElement>(null);
|
||||||
const wasDraggingRef = useRef<boolean>(false);
|
const wasDraggingRef = useRef<boolean>(false);
|
||||||
|
|
||||||
|
// Refs for auto-scroll during playback
|
||||||
|
const pianoRollExpectedScrollLeftRef = useRef<number>(-1);
|
||||||
|
const pianoRollIsPlayingRef = useRef(false);
|
||||||
|
|
||||||
// Ref for storing the setNoteUpdateCounter function
|
// Ref for storing the setNoteUpdateCounter function
|
||||||
const triggerNoteUpdateRef = useRef<React.Dispatch<React.SetStateAction<number>> | null>(null);
|
const triggerNoteUpdateRef = useRef<React.Dispatch<React.SetStateAction<number>> | null>(null);
|
||||||
|
|
||||||
@@ -621,6 +625,52 @@ const PianoRoll: React.FC<PianoRollProps> = ({
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Sync isPlayingRef for use inside scroll event closure
|
||||||
|
useEffect(() => {
|
||||||
|
pianoRollIsPlayingRef.current = isPlaying;
|
||||||
|
}, [isPlaying]);
|
||||||
|
|
||||||
|
// Detect manual horizontal scroll during playback
|
||||||
|
useEffect(() => {
|
||||||
|
const container = pianoRollContentRef.current;
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const handleScroll = () => {
|
||||||
|
if (!pianoRollIsPlayingRef.current) return;
|
||||||
|
if (Math.abs(container.scrollLeft - pianoRollExpectedScrollLeftRef.current) < 1) return;
|
||||||
|
useProjectStore.getState().setAutoScrollEnabled(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
container.addEventListener('scroll', handleScroll);
|
||||||
|
return () => container.removeEventListener('scroll', handleScroll);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Auto-scroll to keep playhead centered during playback
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isPlaying || !autoScrollEnabled) return;
|
||||||
|
|
||||||
|
const container = pianoRollContentRef.current;
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const beatWidth = parseInt(
|
||||||
|
getComputedStyle(document.documentElement).getPropertyValue('--region-grid-beat-width')
|
||||||
|
) || 40;
|
||||||
|
const playheadPixel = playheadPosition * beatWidth;
|
||||||
|
|
||||||
|
// Center the playhead in the visible grid area (excluding the 60px sticky piano keys panel)
|
||||||
|
const keysWidth = parseInt(
|
||||||
|
getComputedStyle(document.documentElement).getPropertyValue('--region-piano-key-width')
|
||||||
|
) || 60;
|
||||||
|
const targetScrollLeft = playheadPixel - (container.clientWidth - keysWidth) / 2;
|
||||||
|
const clampedScrollLeft = Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(targetScrollLeft, container.scrollWidth - container.clientWidth)
|
||||||
|
);
|
||||||
|
|
||||||
|
pianoRollExpectedScrollLeftRef.current = clampedScrollLeft;
|
||||||
|
container.scrollLeft = clampedScrollLeft;
|
||||||
|
}, [playheadPosition, isPlaying, autoScrollEnabled]);
|
||||||
|
|
||||||
// Scroll horizontally to the active region's starting bar
|
// Scroll horizontally to the active region's starting bar
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (pianoRollContentRef.current && activeRegion) {
|
if (pianoRollContentRef.current && activeRegion) {
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ interface ProjectState {
|
|||||||
loopingRange: [number, number]; // [startBar, endBar] - bar indices (0-based)
|
loopingRange: [number, number]; // [startBar, endBar] - bar indices (0-based)
|
||||||
playheadPosition: number; // in beats
|
playheadPosition: number; // in beats
|
||||||
isPlaying: boolean;
|
isPlaying: boolean;
|
||||||
|
autoScrollEnabled: boolean;
|
||||||
currentTime: string; // formatted time string
|
currentTime: string; // formatted time string
|
||||||
|
|
||||||
// Selection state for UI reactivity
|
// Selection state for UI reactivity
|
||||||
@@ -114,6 +115,7 @@ interface ProjectState {
|
|||||||
refreshStatus: () => void;
|
refreshStatus: () => void;
|
||||||
loadProject: (project: KGProject | null, savedName?: string) => Promise<void>;
|
loadProject: (project: KGProject | null, savedName?: string) => Promise<void>;
|
||||||
setPlayheadPosition: (position: number) => void;
|
setPlayheadPosition: (position: number) => void;
|
||||||
|
setAutoScrollEnabled: (enabled: boolean) => void;
|
||||||
startPlaying: () => Promise<void>;
|
startPlaying: () => Promise<void>;
|
||||||
stopPlaying: () => Promise<void>;
|
stopPlaying: () => Promise<void>;
|
||||||
toggleLoop: () => void;
|
toggleLoop: () => void;
|
||||||
@@ -270,6 +272,7 @@ export const useProjectStore = create<ProjectState>((set, get) => {
|
|||||||
loopingRange: currentProject.getLoopingRange(),
|
loopingRange: currentProject.getLoopingRange(),
|
||||||
playheadPosition: KGCore.instance().getPlayheadPosition(),
|
playheadPosition: KGCore.instance().getPlayheadPosition(),
|
||||||
isPlaying: KGCore.instance().getIsPlaying(),
|
isPlaying: KGCore.instance().getIsPlaying(),
|
||||||
|
autoScrollEnabled: true,
|
||||||
currentTime: beatsToTimeString(KGCore.instance().getPlayheadPosition(), currentProject.getBpm(), currentProject.getTimeSignature()),
|
currentTime: beatsToTimeString(KGCore.instance().getPlayheadPosition(), currentProject.getBpm(), currentProject.getTimeSignature()),
|
||||||
|
|
||||||
// Initial selection state
|
// Initial selection state
|
||||||
@@ -752,9 +755,13 @@ export const useProjectStore = create<ProjectState>((set, get) => {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setAutoScrollEnabled: (enabled: boolean) => {
|
||||||
|
set({ autoScrollEnabled: enabled });
|
||||||
|
},
|
||||||
|
|
||||||
startPlaying: async () => {
|
startPlaying: async () => {
|
||||||
await KGCore.instance().startPlaying();
|
await KGCore.instance().startPlaying();
|
||||||
set({ isPlaying: true });
|
set({ isPlaying: true, autoScrollEnabled: true });
|
||||||
},
|
},
|
||||||
|
|
||||||
stopPlaying: async () => {
|
stopPlaying: async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user