feat: implement TimeScale component with proper zoom calculation

- Add TimeScale component with canvas-based rendering
- Use 5 pixels per second base scale (duration * zoom * 5)
- Implement viewport-based rendering for performance
- Add scroll synchronization with waveforms
- Add 240px padding for alignment with track controls and master area
- Apply custom scrollbar styling
- Update all waveform width calculations to match timeline

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-20 10:12:13 +01:00
parent 119c8c2942
commit 477a444c78
5 changed files with 446 additions and 27 deletions

View File

@@ -36,6 +36,9 @@ export interface TrackListProps {
trackLevels?: Record<string, number>;
onParameterTouched?: (trackId: string, laneId: string, touched: boolean) => void;
isPlaying?: boolean;
timeScaleScrollRef?: React.MutableRefObject<HTMLDivElement | null>;
onTimeScaleScroll?: () => void;
timeScaleScrollHandlerRef?: React.MutableRefObject<(() => void) | null>;
}
export function TrackList({
@@ -57,6 +60,9 @@ export function TrackList({
trackLevels = {},
onParameterTouched,
isPlaying = false,
timeScaleScrollRef: externalTimeScaleScrollRef,
onTimeScaleScroll,
timeScaleScrollHandlerRef,
}: TrackListProps) {
const [importDialogOpen, setImportDialogOpen] = React.useState(false);
const [effectBrowserTrackId, setEffectBrowserTrackId] = React.useState<string | null>(null);
@@ -66,6 +72,8 @@ export function TrackList({
// Refs for horizontal scroll synchronization (per track)
const waveformHScrollRefs = React.useRef<Map<string, HTMLDivElement>>(new Map());
const automationHScrollRefs = React.useRef<Map<string, HTMLDivElement>>(new Map());
const localTimeScaleScrollRef = React.useRef<HTMLDivElement | null>(null);
const timeScaleScrollRef = externalTimeScaleScrollRef || localTimeScaleScrollRef;
const [syncingScroll, setSyncingScroll] = React.useState(false);
// Synchronize vertical scroll between controls and waveforms
@@ -100,6 +108,11 @@ export function TrackList({
el.scrollLeft = scrollLeft;
});
// Sync time scale
if (timeScaleScrollRef.current) {
timeScaleScrollRef.current.scrollLeft = scrollLeft;
}
setSyncingScroll(false);
}, [syncingScroll]);
@@ -127,9 +140,50 @@ export function TrackList({
}
});
// Sync time scale
if (timeScaleScrollRef.current) {
timeScaleScrollRef.current.scrollLeft = scrollLeft;
}
setSyncingScroll(false);
}, [syncingScroll]);
const handleTimeScaleScrollInternal = React.useCallback(() => {
if (syncingScroll) return;
setSyncingScroll(true);
if (!timeScaleScrollRef.current) {
setSyncingScroll(false);
return;
}
const scrollLeft = timeScaleScrollRef.current.scrollLeft;
// Sync all waveforms
waveformHScrollRefs.current.forEach((el) => {
el.scrollLeft = scrollLeft;
});
// Sync all automation lanes
automationHScrollRefs.current.forEach((el) => {
el.scrollLeft = scrollLeft;
});
setSyncingScroll(false);
// Also call the external callback if provided
if (onTimeScaleScroll) {
onTimeScaleScroll();
}
}, [syncingScroll, onTimeScaleScroll]);
// Expose the scroll handler via ref so AudioEditor can call it
React.useEffect(() => {
if (timeScaleScrollHandlerRef) {
timeScaleScrollHandlerRef.current = handleTimeScaleScrollInternal;
}
}, [handleTimeScaleScrollInternal, timeScaleScrollHandlerRef]);
const handleImportTrack = (buffer: AudioBuffer, name: string) => {
if (onImportTrack) {
onImportTrack(buffer, name);
@@ -490,7 +544,7 @@ export function TrackList({
<div className="overflow-x-auto custom-scrollbar">
<div
style={{
minWidth: duration && zoom > 1 ? `${duration * zoom * 100}px` : '100%',
minWidth: duration && zoom >= 1 ? `${duration * zoom * 5}px` : '100%',
}}
>
{track.automation.lanes
@@ -799,7 +853,7 @@ export function TrackList({
<div
className="h-full"
style={{
minWidth: duration && zoom > 1 ? `${duration * zoom * 100}px` : '100%',
minWidth: duration && zoom >= 1 ? `${duration * zoom * 5}px` : '100%',
}}
>
<Track
@@ -1072,7 +1126,7 @@ export function TrackList({
>
<div
style={{
minWidth: duration && zoom > 1 ? `${duration * zoom * 100}px` : '100%',
minWidth: duration && zoom >= 1 ? `${duration * zoom * 5}px` : '100%',
}}
>
{track.automation.lanes