From d4b87aafe3b44209785de9695fe93ec374211aab Mon Sep 17 00:00:00 2001 From: JSC Date: Wed, 3 Sep 2025 20:51:14 +0200 Subject: [PATCH] feat: enhance time interval calculation for zoom level in SequencerCanvas --- src/components/sequencer/SequencerCanvas.tsx | 153 ++++++++++++++++--- 1 file changed, 131 insertions(+), 22 deletions(-) diff --git a/src/components/sequencer/SequencerCanvas.tsx b/src/components/sequencer/SequencerCanvas.tsx index 824ec91..77f2385 100644 --- a/src/components/sequencer/SequencerCanvas.tsx +++ b/src/components/sequencer/SequencerCanvas.tsx @@ -125,6 +125,52 @@ function TrackRow({ track, duration, zoom, isPlaying, currentTime, draggedItem, onRemoveSound(soundId, track.id) } + // Calculate logical time intervals based on zoom level (same as main component) + const getTimeIntervals = (zoom: number, duration: number) => { + const durationSeconds = duration / 1000 + const minorIntervals: number[] = [] + const majorIntervals: number[] = [] + + // Define logical interval progressions + const intervalSets = [ + { minor: 1, major: 5 }, // 1s minor, 5s major (very zoomed in) + { minor: 5, major: 30 }, // 5s minor, 30s major + { minor: 10, major: 60 }, // 10s minor, 1min major + { minor: 30, major: 300 }, // 30s minor, 5min major + { minor: 60, major: 600 }, // 1min minor, 10min major + { minor: 300, major: 1800 }, // 5min minor, 30min major + { minor: 600, major: 3600 } // 10min minor, 1h major + ] + + // Find appropriate interval set based on zoom level + // We want major intervals to be roughly 100-200px apart + const targetMajorSpacing = 150 + + let selectedIntervals = intervalSets[intervalSets.length - 1] // fallback to largest + for (const intervals of intervalSets) { + if (intervals.major * zoom >= targetMajorSpacing) { + selectedIntervals = intervals + break + } + } + + // Generate minor intervals (every interval) + for (let i = 0; i * selectedIntervals.minor <= durationSeconds; i++) { + const time = i * selectedIntervals.minor + minorIntervals.push(time) + } + + // Generate major intervals (at major boundaries) + for (let i = 0; i * selectedIntervals.major <= durationSeconds; i++) { + const time = i * selectedIntervals.major + majorIntervals.push(time) + } + + return { minorIntervals, majorIntervals, minorInterval: selectedIntervals.minor, majorInterval: selectedIntervals.major } + } + + const { minorIntervals, majorIntervals } = getTimeIntervals(zoom, duration) + return (
{/* Grid lines for time markers */}
- {Array.from({ length: Math.ceil(duration / 1000) + 1 }).map((_, i) => ( + {/* Minor grid lines */} + {minorIntervals.map((time) => (
))} - {/* Major grid lines every 10 seconds */} - {Array.from({ length: Math.floor(duration / 10000) + 1 }).map((_, i) => ( + {/* Major grid lines */} + {majorIntervals.map((time) => (
))}
@@ -228,7 +275,6 @@ export const SequencerCanvas = forwardRef( }, ref) => { const totalWidth = (duration / 1000) * zoom // Convert ms to seconds for zoom calculation const timelineRef = useRef(null) - const containerRef = useRef(null) // Add a fallback droppable for the entire canvas area const { setNodeRef: setCanvasDropRef } = useDroppable({ @@ -238,6 +284,53 @@ export const SequencerCanvas = forwardRef( }, }) + // Calculate logical time intervals based on zoom level + const getTimeIntervals = (zoom: number, duration: number) => { + const durationSeconds = duration / 1000 + const minorIntervals: number[] = [] + const majorIntervals: number[] = [] + + // Define logical interval progressions + const intervalSets = [ + { minor: 0.1, major: 1 }, // 0.1s minor, 1s major (mega zoomed in) + { minor: 1, major: 5 }, // 1s minor, 5s major (very zoomed in) + { minor: 5, major: 30 }, // 5s minor, 30s major + { minor: 10, major: 60 }, // 10s minor, 1min major + { minor: 30, major: 300 }, // 30s minor, 5min major + { minor: 60, major: 600 }, // 1min minor, 10min major + { minor: 300, major: 1800 }, // 5min minor, 30min major + { minor: 600, major: 3600 } // 10min minor, 1h major + ] + + // Find appropriate interval set based on zoom level + // We want major intervals to be roughly 100-200px apart + const targetMajorSpacing = 150 + + let selectedIntervals = intervalSets[intervalSets.length - 1] // fallback to largest + for (const intervals of intervalSets) { + if (intervals.major * zoom >= targetMajorSpacing) { + selectedIntervals = intervals + break + } + } + + // Generate minor intervals (every interval) + for (let i = 0; i * selectedIntervals.minor <= durationSeconds; i++) { + const time = i * selectedIntervals.minor + minorIntervals.push(time) + } + + // Generate major intervals (at major boundaries) + for (let i = 0; i * selectedIntervals.major <= durationSeconds; i++) { + const time = i * selectedIntervals.major + majorIntervals.push(time) + } + + return { minorIntervals, majorIntervals, minorInterval: selectedIntervals.minor, majorInterval: selectedIntervals.major } + } + + const { minorIntervals, majorIntervals } = getTimeIntervals(zoom, duration) + const handleTracksScroll = (e: React.UIEvent) => { // Sync timeline horizontal scroll with tracks if (timelineRef.current) { @@ -264,23 +357,39 @@ export const SequencerCanvas = forwardRef( }} >
- {Array.from({ length: Math.ceil(duration / 1000) + 1 }).map((_, i) => ( -
- {/* Time markers */} - {i % 5 === 0 && ( - <> -
-
- {Math.floor(i / 60)}:{(i % 60).toString().padStart(2, '0')} -
- - )} - {i % 5 !== 0 && ( -
- )} + {/* Minor time markers */} + {minorIntervals.map((time) => ( +
+
))} + {/* Major time markers with labels */} + {majorIntervals.map((time) => { + const formatTime = (seconds: number): string => { + if (seconds < 60) { + // For times under 1 minute, show seconds with decimal places if needed + return seconds < 10 && seconds % 1 !== 0 + ? seconds.toFixed(1) + 's' + : Math.floor(seconds) + 's' + } else { + // For times over 1 minute, show MM:SS format + const mins = Math.floor(seconds / 60) + const secs = Math.floor(seconds % 60) + return `${mins}:${secs.toString().padStart(2, '0')}` + } + } + + return ( +
+
+
+ {formatTime(time)} +
+
+ ) + })} + {/* Playhead in ruler */} {isPlaying && (