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

@@ -24,6 +24,7 @@ const ImportTrackDialog = React.lazy(() => import('@/components/tracks/ImportTra
const KeyboardShortcutsDialog = React.lazy(() => import('@/components/dialogs/KeyboardShortcutsDialog').then(m => ({ default: m.KeyboardShortcutsDialog })));
const MarkerTimeline = React.lazy(() => import('@/components/markers/MarkerTimeline').then(m => ({ default: m.MarkerTimeline })));
const MarkerDialog = React.lazy(() => import('@/components/markers/MarkerDialog').then(m => ({ default: m.MarkerDialog })));
const TimeScale = React.lazy(() => import('@/components/timeline/TimeScale').then(m => ({ default: m.TimeScale })));
// Lazy load analysis components (shown conditionally based on analyzerView)
const FrequencyAnalyzer = React.lazy(() => import('@/components/analysis/FrequencyAnalyzer').then(m => ({ default: m.FrequencyAnalyzer })));
@@ -197,6 +198,16 @@ export function AudioEditor() {
// Track last recorded values to detect changes
const lastRecordedValuesRef = React.useRef<Map<string, { value: number; time: number }>>(new Map());
// Time scale scroll synchronization
const timeScaleScrollRef = React.useRef<HTMLDivElement | null>(null);
const timeScaleScrollHandlerRef = React.useRef<(() => void) | null>(null);
const handleTimeScaleScroll = React.useCallback(() => {
if (timeScaleScrollHandlerRef.current) {
timeScaleScrollHandlerRef.current();
}
}, []);
// Automation recording callback
const handleAutomationRecording = React.useCallback((
trackId: string,
@@ -2056,23 +2067,22 @@ export function AudioEditor() {
</header>
{/* Main content area */}
<div className="flex flex-1 overflow-hidden">
{/* Main canvas area */}
<main className="flex-1 flex flex-col overflow-hidden bg-background">
{/* Multi-Track View */}
<div className="flex-1 flex flex-col overflow-hidden">
{/* Marker Timeline */}
<React.Suspense fallback={null}>
<MarkerTimeline
markers={markers}
duration={duration}
currentTime={currentTime}
onMarkerEdit={handleEditMarker}
onMarkerDelete={handleDeleteMarker}
onSeek={seek}
/>
</React.Suspense>
<div className="flex flex-1 flex-col overflow-hidden">
{/* Time Scale - Full width */}
<React.Suspense fallback={null}>
<TimeScale
duration={duration}
zoom={zoom}
currentTime={currentTime}
onSeek={seek}
scrollRef={timeScaleScrollRef}
onScroll={handleTimeScaleScroll}
/>
</React.Suspense>
<div className="flex flex-1 overflow-hidden">
{/* Main canvas area */}
<main className="flex-1 flex flex-col overflow-hidden bg-background">
<TrackList
tracks={tracks}
zoom={zoom}
@@ -2087,18 +2097,20 @@ export function AudioEditor() {
onSeek={seek}
onSelectionChange={handleSelectionChange}
onToggleRecordEnable={handleToggleRecordEnable}
timeScaleScrollRef={timeScaleScrollRef}
onTimeScaleScroll={handleTimeScaleScroll}
timeScaleScrollHandlerRef={timeScaleScrollHandlerRef}
recordingTrackId={recordingTrackId}
recordingLevel={recordingState.inputLevel}
trackLevels={trackLevels}
onParameterTouched={setParameterTouched}
isPlaying={isPlaying}
/>
</div>
</main>
</main>
{/* Right Sidebar - Master Controls & Analyzers - Hidden on mobile */}
<aside className="hidden lg:flex flex-shrink-0 border-l border-border bg-card flex-col pt-5 px-4 pb-4 gap-4 w-60">
{/* Master Controls */}
{/* Right Sidebar - Master Controls & Analyzers - Hidden on mobile */}
<aside className="hidden lg:flex flex-shrink-0 border-l border-border bg-card flex-col pt-5 px-4 pb-4 gap-4 w-60">
{/* Master Controls */}
<div className="flex items-center justify-center">
<MasterControls
volume={masterVolume}
@@ -2215,7 +2227,8 @@ export function AudioEditor() {
)}
</div>
</div>
</aside>
</aside>
</div>
</div>
{/* Bottom Bar - Stacked on mobile (Master then Transport), Side-by-side on desktop */}