diff --git a/src/components/sequencer/SequencerCanvas.tsx b/src/components/sequencer/SequencerCanvas.tsx
index daebda3..719c092 100644
--- a/src/components/sequencer/SequencerCanvas.tsx
+++ b/src/components/sequencer/SequencerCanvas.tsx
@@ -14,6 +14,7 @@ interface SequencerCanvasProps {
draggedItem?: any // Current dragged item from parent
dragOverInfo?: {trackId: string, x: number} | null // Drag over position info
onRemoveSound: (soundId: string, trackId: string) => void
+ timeIntervals: {minorIntervals: number[], majorIntervals: number[], minorInterval: number, majorInterval: number}
}
interface TrackRowProps {
@@ -25,6 +26,7 @@ interface TrackRowProps {
draggedItem?: any // Current dragged item
dragOverInfo?: {trackId: string, x: number} | null // Drag over position info
onRemoveSound: (soundId: string, trackId: string) => void
+ timeIntervals: {minorIntervals: number[], majorIntervals: number[], minorInterval: number, majorInterval: number}
}
interface PlacedSoundItemProps {
@@ -32,9 +34,10 @@ interface PlacedSoundItemProps {
zoom: number
trackId: string
onRemove: (soundId: string) => void
+ minorInterval: number
}
-function PlacedSoundItem({ sound, zoom, trackId, onRemove }: PlacedSoundItemProps) {
+function PlacedSoundItem({ sound, zoom, trackId, onRemove, minorInterval }: PlacedSoundItemProps) {
const {
attributes,
listeners,
@@ -54,18 +57,11 @@ function PlacedSoundItem({ sound, zoom, trackId, onRemove }: PlacedSoundItemProp
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
} : undefined
- // Helper function to snap time to 100ms intervals
- const snapToGrid = (timeInSeconds: number): number => {
- const snapIntervalMs = 100 // 100ms snap interval
- const timeInMs = timeInSeconds * 1000
- const snappedMs = Math.round(timeInMs / snapIntervalMs) * snapIntervalMs
- return snappedMs / 1000 // Convert back to seconds
- }
-
const width = (sound.duration / 1000) * zoom // Convert ms to seconds for zoom calculation
- // Ensure placed sounds are positioned at snapped locations
- const snappedStartTime = snapToGrid(sound.startTime / 1000)
- const left = snappedStartTime * zoom
+ // Ensure placed sounds are positioned at snapped locations using current minor interval
+ const startTimeSeconds = sound.startTime / 1000
+ const snappedStartTime = Math.round(startTimeSeconds / minorInterval) * minorInterval
+ const left = Math.max(0, snappedStartTime) * zoom
const formatTime = (seconds: number): string => {
const mins = Math.floor(seconds / 60)
@@ -119,7 +115,7 @@ function PlacedSoundItem({ sound, zoom, trackId, onRemove }: PlacedSoundItemProp
)
}
-function TrackRow({ track, duration, zoom, isPlaying, currentTime, draggedItem, dragOverInfo, onRemoveSound }: TrackRowProps) {
+function TrackRow({ track, duration, zoom, isPlaying, currentTime, draggedItem, dragOverInfo, onRemoveSound, timeIntervals }: TrackRowProps) {
const playheadPosition = (currentTime / 1000) * zoom // Convert ms to seconds for zoom calculation
const { isOver, setNodeRef: setDropRef } = useDroppable({
@@ -134,51 +130,7 @@ 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)
+ const { minorIntervals, majorIntervals } = timeIntervals
return (
@@ -266,6 +218,7 @@ function TrackRow({ track, duration, zoom, isPlaying, currentTime, draggedItem,
zoom={zoom}
trackId={track.id}
onRemove={handleRemoveSound}
+ minorInterval={timeIntervals.minorInterval}
/>
))}
@@ -283,6 +236,7 @@ export const SequencerCanvas = forwardRef(
draggedItem,
dragOverInfo,
onRemoveSound,
+ timeIntervals,
}, ref) => {
const totalWidth = (duration / 1000) * zoom // Convert ms to seconds for zoom calculation
const timelineRef = useRef(null)
@@ -295,52 +249,7 @@ 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 { minorIntervals, majorIntervals } = timeIntervals
const handleTracksScroll = (e: React.UIEvent) => {
// Sync timeline horizontal scroll with tracks
@@ -430,6 +339,7 @@ export const SequencerCanvas = forwardRef(
draggedItem={draggedItem}
dragOverInfo={dragOverInfo}
onRemoveSound={onRemoveSound}
+ timeIntervals={timeIntervals}
/>
))}
diff --git a/src/pages/SequencerPage.tsx b/src/pages/SequencerPage.tsx
index 931535f..81d8b29 100644
--- a/src/pages/SequencerPage.tsx
+++ b/src/pages/SequencerPage.tsx
@@ -79,14 +79,58 @@ export function SequencerPage() {
}
}, [])
- // Helper function to snap time to 100ms intervals with improved precision
- const snapToGrid = useCallback((timeInSeconds: number): number => {
- const snapIntervalMs = 100 // 100ms snap interval
- const timeInMs = Math.max(0, timeInSeconds * 1000) // Ensure non-negative
- const snappedMs = Math.round(timeInMs / snapIntervalMs) * snapIntervalMs
- return Math.max(0, snappedMs / 1000) // Convert back to seconds, ensure non-negative
+ // Calculate logical time intervals based on zoom level (shared with SequencerCanvas)
+ const getTimeIntervals = useCallback((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 }
}, [])
+ // Helper function to snap time to the current minor interval
+ const snapToGrid = useCallback((timeInSeconds: number, zoom: number, duration: number): number => {
+ const { minorInterval } = getTimeIntervals(zoom, duration)
+ const snappedTime = Math.round(timeInSeconds / minorInterval) * minorInterval
+ return Math.max(0, snappedTime) // Ensure non-negative
+ }, [getTimeIntervals])
+
// Update drag over info based on current mouse position and over target
useEffect(() => {
if (draggedItem && currentMousePos && (draggedItem.type === 'sound' || draggedItem.type === 'placed-sound')) {
@@ -102,9 +146,9 @@ export function SequencerPage() {
currentMousePos.y <= rect.bottom
) {
const rawX = currentMousePos.x - rect.left
- // Apply snapping to the drag over position for consistency
+ // Apply adaptive snapping to the drag over position
const rawTimeSeconds = rawX / state.zoom
- const snappedTimeSeconds = snapToGrid(rawTimeSeconds)
+ const snappedTimeSeconds = snapToGrid(rawTimeSeconds, state.zoom, state.duration)
const snappedX = snappedTimeSeconds * state.zoom
setDragOverInfo({ trackId: track.id, x: Math.max(0, snappedX) })
return
@@ -116,7 +160,7 @@ export function SequencerPage() {
} else {
setDragOverInfo(null)
}
- }, [draggedItem, currentMousePos, state.tracks, state.zoom, snapToGrid])
+ }, [draggedItem, currentMousePos, state.tracks, state.zoom, state.duration, snapToGrid])
const handleDragEnd = useCallback((event: DragEndEvent) => {
const { active, over } = event
@@ -380,6 +424,7 @@ export function SequencerPage() {
draggedItem={draggedItem}
dragOverInfo={dragOverInfo}
onRemoveSound={handleRemoveSound}
+ timeIntervals={getTimeIntervals(state.zoom, state.duration)}
/>