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:
93
lib/utils/timeline.ts
Normal file
93
lib/utils/timeline.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Timeline coordinate conversion and formatting utilities
|
||||
*/
|
||||
|
||||
/**
|
||||
* Base pixels per second at zoom level 1
|
||||
* zoom=1: 5 pixels per second
|
||||
* zoom=2: 10 pixels per second, etc.
|
||||
*/
|
||||
const PIXELS_PER_SECOND_BASE = 5;
|
||||
|
||||
/**
|
||||
* Convert time (in seconds) to pixel position
|
||||
*/
|
||||
export function timeToPixel(time: number, duration: number, zoom: number): number {
|
||||
if (duration === 0) return 0;
|
||||
const totalWidth = duration * zoom * PIXELS_PER_SECOND_BASE;
|
||||
return (time / duration) * totalWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert pixel position to time (in seconds)
|
||||
*/
|
||||
export function pixelToTime(pixel: number, duration: number, zoom: number): number {
|
||||
if (duration === 0) return 0;
|
||||
const totalWidth = duration * zoom * PIXELS_PER_SECOND_BASE;
|
||||
return (pixel / totalWidth) * duration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate appropriate tick interval based on visible duration
|
||||
* Returns interval in seconds
|
||||
*/
|
||||
export function calculateTickInterval(visibleDuration: number): {
|
||||
major: number;
|
||||
minor: number;
|
||||
} {
|
||||
// Very zoomed in: show sub-second intervals
|
||||
if (visibleDuration < 5) {
|
||||
return { major: 1, minor: 0.5 };
|
||||
}
|
||||
// Zoomed in: show every second
|
||||
if (visibleDuration < 20) {
|
||||
return { major: 5, minor: 1 };
|
||||
}
|
||||
// Medium zoom: show every 5 seconds
|
||||
if (visibleDuration < 60) {
|
||||
return { major: 10, minor: 5 };
|
||||
}
|
||||
// Zoomed out: show every 10 seconds
|
||||
if (visibleDuration < 300) {
|
||||
return { major: 30, minor: 10 };
|
||||
}
|
||||
// Very zoomed out: show every minute
|
||||
return { major: 60, minor: 30 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Format time in seconds to display format
|
||||
* Returns format like "0:00", "1:23", "12:34.5"
|
||||
*/
|
||||
export function formatTimeLabel(seconds: number, showMillis: boolean = false): string {
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = seconds % 60;
|
||||
|
||||
if (showMillis) {
|
||||
const wholeSecs = Math.floor(secs);
|
||||
const decimalPart = Math.floor((secs - wholeSecs) * 10);
|
||||
return `${mins}:${wholeSecs.toString().padStart(2, '0')}.${decimalPart}`;
|
||||
}
|
||||
|
||||
return `${mins}:${Math.floor(secs).toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate visible time range based on scroll position
|
||||
*/
|
||||
export function getVisibleTimeRange(
|
||||
scrollLeft: number,
|
||||
viewportWidth: number,
|
||||
duration: number,
|
||||
zoom: number
|
||||
): { start: number; end: number } {
|
||||
const totalWidth = duration * zoom * 100;
|
||||
|
||||
const start = pixelToTime(scrollLeft, duration, zoom);
|
||||
const end = pixelToTime(scrollLeft + viewportWidth, duration, zoom);
|
||||
|
||||
return {
|
||||
start: Math.max(0, start),
|
||||
end: Math.min(duration, end),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user