feat: added auto-center playhead to piano roll window
This commit is contained in:
@@ -33,6 +33,8 @@ const MainContent: React.FC<MainContentProps> = ({
|
||||
setPlayheadPosition,
|
||||
playheadPosition,
|
||||
isPlaying,
|
||||
autoScrollEnabled,
|
||||
setAutoScrollEnabled,
|
||||
clearAllSelections,
|
||||
setSelectedTrack,
|
||||
showPianoRoll,
|
||||
@@ -84,7 +86,6 @@ const MainContent: React.FC<MainContentProps> = ({
|
||||
|
||||
// Refs for auto-scroll during playback
|
||||
const mainContentRef = useRef<HTMLDivElement | null>(null);
|
||||
const userManuallyScrolledRef = useRef(false);
|
||||
const expectedScrollLeftRef = useRef<number>(-1);
|
||||
const isPlayingRef = useRef(false);
|
||||
|
||||
@@ -95,11 +96,8 @@ const MainContent: React.FC<MainContentProps> = ({
|
||||
const loopDragStartXRef = useRef<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(() => {
|
||||
if (isPlaying && !isPlayingRef.current) {
|
||||
userManuallyScrolledRef.current = false;
|
||||
}
|
||||
isPlayingRef.current = isPlaying;
|
||||
}, [isPlaying]);
|
||||
|
||||
@@ -110,9 +108,8 @@ const MainContent: React.FC<MainContentProps> = ({
|
||||
|
||||
const handleScroll = () => {
|
||||
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;
|
||||
userManuallyScrolledRef.current = true;
|
||||
useProjectStore.getState().setAutoScrollEnabled(false);
|
||||
};
|
||||
|
||||
container.addEventListener('scroll', handleScroll);
|
||||
@@ -121,7 +118,7 @@ const MainContent: React.FC<MainContentProps> = ({
|
||||
|
||||
// Auto-scroll to keep playhead centered during playback
|
||||
useEffect(() => {
|
||||
if (!isPlaying || userManuallyScrolledRef.current) return;
|
||||
if (!isPlaying || !autoScrollEnabled) return;
|
||||
|
||||
const container = mainContentRef.current;
|
||||
if (!container) return;
|
||||
@@ -143,7 +140,7 @@ const MainContent: React.FC<MainContentProps> = ({
|
||||
|
||||
expectedScrollLeftRef.current = clampedScrollLeft;
|
||||
container.scrollLeft = clampedScrollLeft;
|
||||
}, [playheadPosition, isPlaying, timeSignature]);
|
||||
}, [playheadPosition, isPlaying, autoScrollEnabled, timeSignature]);
|
||||
|
||||
// Effect to verify track updates
|
||||
useEffect(() => {
|
||||
|
||||
@@ -29,7 +29,7 @@ const PianoRoll: React.FC<PianoRollProps> = ({
|
||||
initialPosition,
|
||||
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
|
||||
const [activeTool, setActiveTool] = useState<'pointer' | 'pencil'>('pointer');
|
||||
@@ -58,7 +58,11 @@ const PianoRoll: React.FC<PianoRollProps> = ({
|
||||
const pianoRollContentRef = useRef<HTMLDivElement>(null);
|
||||
const pianoGridRef = useRef<HTMLDivElement>(null);
|
||||
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
|
||||
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
|
||||
useEffect(() => {
|
||||
if (pianoRollContentRef.current && activeRegion) {
|
||||
|
||||
@@ -62,6 +62,7 @@ interface ProjectState {
|
||||
loopingRange: [number, number]; // [startBar, endBar] - bar indices (0-based)
|
||||
playheadPosition: number; // in beats
|
||||
isPlaying: boolean;
|
||||
autoScrollEnabled: boolean;
|
||||
currentTime: string; // formatted time string
|
||||
|
||||
// Selection state for UI reactivity
|
||||
@@ -114,6 +115,7 @@ interface ProjectState {
|
||||
refreshStatus: () => void;
|
||||
loadProject: (project: KGProject | null, savedName?: string) => Promise<void>;
|
||||
setPlayheadPosition: (position: number) => void;
|
||||
setAutoScrollEnabled: (enabled: boolean) => void;
|
||||
startPlaying: () => Promise<void>;
|
||||
stopPlaying: () => Promise<void>;
|
||||
toggleLoop: () => void;
|
||||
@@ -270,6 +272,7 @@ export const useProjectStore = create<ProjectState>((set, get) => {
|
||||
loopingRange: currentProject.getLoopingRange(),
|
||||
playheadPosition: KGCore.instance().getPlayheadPosition(),
|
||||
isPlaying: KGCore.instance().getIsPlaying(),
|
||||
autoScrollEnabled: true,
|
||||
currentTime: beatsToTimeString(KGCore.instance().getPlayheadPosition(), currentProject.getBpm(), currentProject.getTimeSignature()),
|
||||
|
||||
// Initial selection state
|
||||
@@ -752,9 +755,13 @@ export const useProjectStore = create<ProjectState>((set, get) => {
|
||||
});
|
||||
},
|
||||
|
||||
setAutoScrollEnabled: (enabled: boolean) => {
|
||||
set({ autoScrollEnabled: enabled });
|
||||
},
|
||||
|
||||
startPlaying: async () => {
|
||||
await KGCore.instance().startPlaying();
|
||||
set({ isPlaying: true });
|
||||
set({ isPlaying: true, autoScrollEnabled: true });
|
||||
},
|
||||
|
||||
stopPlaying: async () => {
|
||||
|
||||
Reference in New Issue
Block a user