feat: enhance time interval calculation for zoom level in SequencerCanvas
This commit is contained in:
@@ -125,6 +125,52 @@ function TrackRow({ track, duration, zoom, isPlaying, currentTime, draggedItem,
|
|||||||
onRemoveSound(soundId, track.id)
|
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 (
|
return (
|
||||||
<div className="relative" style={{ height: '80px' }}>
|
<div className="relative" style={{ height: '80px' }}>
|
||||||
<div
|
<div
|
||||||
@@ -139,19 +185,20 @@ function TrackRow({ track, duration, zoom, isPlaying, currentTime, draggedItem,
|
|||||||
>
|
>
|
||||||
{/* Grid lines for time markers */}
|
{/* Grid lines for time markers */}
|
||||||
<div className="absolute inset-0 pointer-events-none">
|
<div className="absolute inset-0 pointer-events-none">
|
||||||
{Array.from({ length: Math.ceil(duration / 1000) + 1 }).map((_, i) => (
|
{/* Minor grid lines */}
|
||||||
|
{minorIntervals.map((time) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={`minor-${time}`}
|
||||||
className="absolute top-0 bottom-0 w-px bg-border/30"
|
className="absolute top-0 bottom-0 w-px bg-border/30"
|
||||||
style={{ left: `${i * zoom}px` }}
|
style={{ left: `${time * zoom}px` }}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{/* Major grid lines every 10 seconds */}
|
{/* Major grid lines */}
|
||||||
{Array.from({ length: Math.floor(duration / 10000) + 1 }).map((_, i) => (
|
{majorIntervals.map((time) => (
|
||||||
<div
|
<div
|
||||||
key={`major-${i}`}
|
key={`major-${time}`}
|
||||||
className="absolute top-0 bottom-0 w-px bg-border/60"
|
className="absolute top-0 bottom-0 w-px bg-border/60"
|
||||||
style={{ left: `${i * 10 * zoom}px` }}
|
style={{ left: `${time * zoom}px` }}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -228,7 +275,6 @@ export const SequencerCanvas = forwardRef<HTMLDivElement, SequencerCanvasProps>(
|
|||||||
}, ref) => {
|
}, ref) => {
|
||||||
const totalWidth = (duration / 1000) * zoom // Convert ms to seconds for zoom calculation
|
const totalWidth = (duration / 1000) * zoom // Convert ms to seconds for zoom calculation
|
||||||
const timelineRef = useRef<HTMLDivElement>(null)
|
const timelineRef = useRef<HTMLDivElement>(null)
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
|
||||||
|
|
||||||
// Add a fallback droppable for the entire canvas area
|
// Add a fallback droppable for the entire canvas area
|
||||||
const { setNodeRef: setCanvasDropRef } = useDroppable({
|
const { setNodeRef: setCanvasDropRef } = useDroppable({
|
||||||
@@ -238,6 +284,53 @@ export const SequencerCanvas = forwardRef<HTMLDivElement, SequencerCanvasProps>(
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 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<HTMLDivElement>) => {
|
const handleTracksScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
||||||
// Sync timeline horizontal scroll with tracks
|
// Sync timeline horizontal scroll with tracks
|
||||||
if (timelineRef.current) {
|
if (timelineRef.current) {
|
||||||
@@ -264,23 +357,39 @@ export const SequencerCanvas = forwardRef<HTMLDivElement, SequencerCanvasProps>(
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="relative h-full" style={{ width: `${totalWidth}px` }}>
|
<div className="relative h-full" style={{ width: `${totalWidth}px` }}>
|
||||||
{Array.from({ length: Math.ceil(duration / 1000) + 1 }).map((_, i) => (
|
{/* Minor time markers */}
|
||||||
<div key={i} className="absolute top-0 bottom-0" style={{ left: `${i * zoom}px` }}>
|
{minorIntervals.map((time) => (
|
||||||
{/* Time markers */}
|
<div key={`ruler-minor-${time}`} className="absolute top-0 bottom-0" style={{ left: `${time * zoom}px` }}>
|
||||||
{i % 5 === 0 && (
|
<div className="absolute top-0 w-px h-2 bg-border/40" />
|
||||||
<>
|
|
||||||
<div className="absolute top-0 w-px h-3 bg-border/60" />
|
|
||||||
<div className="absolute top-4 text-xs text-muted-foreground font-mono">
|
|
||||||
{Math.floor(i / 60)}:{(i % 60).toString().padStart(2, '0')}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{i % 5 !== 0 && (
|
|
||||||
<div className="absolute top-0 w-px h-2 bg-border/40" />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
{/* 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 (
|
||||||
|
<div key={`ruler-major-${time}`} className="absolute top-0 bottom-0" style={{ left: `${time * zoom}px` }}>
|
||||||
|
<div className="absolute top-0 w-px h-3 bg-border/60" />
|
||||||
|
<div className="absolute top-4 text-xs text-muted-foreground font-mono whitespace-nowrap">
|
||||||
|
{formatTime(time)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
|
||||||
{/* Playhead in ruler */}
|
{/* Playhead in ruler */}
|
||||||
{isPlaying && (
|
{isPlaying && (
|
||||||
<div
|
<div
|
||||||
|
|||||||
Reference in New Issue
Block a user