feat: enhance time snapping and interval calculation for improved sound placement in Sequencer
This commit is contained in:
@@ -14,6 +14,7 @@ interface SequencerCanvasProps {
|
|||||||
draggedItem?: any // Current dragged item from parent
|
draggedItem?: any // Current dragged item from parent
|
||||||
dragOverInfo?: {trackId: string, x: number} | null // Drag over position info
|
dragOverInfo?: {trackId: string, x: number} | null // Drag over position info
|
||||||
onRemoveSound: (soundId: string, trackId: string) => void
|
onRemoveSound: (soundId: string, trackId: string) => void
|
||||||
|
timeIntervals: {minorIntervals: number[], majorIntervals: number[], minorInterval: number, majorInterval: number}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TrackRowProps {
|
interface TrackRowProps {
|
||||||
@@ -25,6 +26,7 @@ interface TrackRowProps {
|
|||||||
draggedItem?: any // Current dragged item
|
draggedItem?: any // Current dragged item
|
||||||
dragOverInfo?: {trackId: string, x: number} | null // Drag over position info
|
dragOverInfo?: {trackId: string, x: number} | null // Drag over position info
|
||||||
onRemoveSound: (soundId: string, trackId: string) => void
|
onRemoveSound: (soundId: string, trackId: string) => void
|
||||||
|
timeIntervals: {minorIntervals: number[], majorIntervals: number[], minorInterval: number, majorInterval: number}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PlacedSoundItemProps {
|
interface PlacedSoundItemProps {
|
||||||
@@ -32,9 +34,10 @@ interface PlacedSoundItemProps {
|
|||||||
zoom: number
|
zoom: number
|
||||||
trackId: string
|
trackId: string
|
||||||
onRemove: (soundId: string) => void
|
onRemove: (soundId: string) => void
|
||||||
|
minorInterval: number
|
||||||
}
|
}
|
||||||
|
|
||||||
function PlacedSoundItem({ sound, zoom, trackId, onRemove }: PlacedSoundItemProps) {
|
function PlacedSoundItem({ sound, zoom, trackId, onRemove, minorInterval }: PlacedSoundItemProps) {
|
||||||
const {
|
const {
|
||||||
attributes,
|
attributes,
|
||||||
listeners,
|
listeners,
|
||||||
@@ -54,18 +57,11 @@ function PlacedSoundItem({ sound, zoom, trackId, onRemove }: PlacedSoundItemProp
|
|||||||
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
|
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
|
||||||
} : undefined
|
} : 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
|
const width = (sound.duration / 1000) * zoom // Convert ms to seconds for zoom calculation
|
||||||
// Ensure placed sounds are positioned at snapped locations
|
// Ensure placed sounds are positioned at snapped locations using current minor interval
|
||||||
const snappedStartTime = snapToGrid(sound.startTime / 1000)
|
const startTimeSeconds = sound.startTime / 1000
|
||||||
const left = snappedStartTime * zoom
|
const snappedStartTime = Math.round(startTimeSeconds / minorInterval) * minorInterval
|
||||||
|
const left = Math.max(0, snappedStartTime) * zoom
|
||||||
|
|
||||||
const formatTime = (seconds: number): string => {
|
const formatTime = (seconds: number): string => {
|
||||||
const mins = Math.floor(seconds / 60)
|
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 playheadPosition = (currentTime / 1000) * zoom // Convert ms to seconds for zoom calculation
|
||||||
|
|
||||||
const { isOver, setNodeRef: setDropRef } = useDroppable({
|
const { isOver, setNodeRef: setDropRef } = useDroppable({
|
||||||
@@ -134,51 +130,7 @@ 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 { minorIntervals, majorIntervals } = timeIntervals
|
||||||
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' }}>
|
||||||
@@ -266,6 +218,7 @@ function TrackRow({ track, duration, zoom, isPlaying, currentTime, draggedItem,
|
|||||||
zoom={zoom}
|
zoom={zoom}
|
||||||
trackId={track.id}
|
trackId={track.id}
|
||||||
onRemove={handleRemoveSound}
|
onRemove={handleRemoveSound}
|
||||||
|
minorInterval={timeIntervals.minorInterval}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -283,6 +236,7 @@ export const SequencerCanvas = forwardRef<HTMLDivElement, SequencerCanvasProps>(
|
|||||||
draggedItem,
|
draggedItem,
|
||||||
dragOverInfo,
|
dragOverInfo,
|
||||||
onRemoveSound,
|
onRemoveSound,
|
||||||
|
timeIntervals,
|
||||||
}, 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)
|
||||||
@@ -295,52 +249,7 @@ export const SequencerCanvas = forwardRef<HTMLDivElement, SequencerCanvasProps>(
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Calculate logical time intervals based on zoom level
|
const { minorIntervals, majorIntervals } = timeIntervals
|
||||||
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
|
||||||
@@ -430,6 +339,7 @@ export const SequencerCanvas = forwardRef<HTMLDivElement, SequencerCanvasProps>(
|
|||||||
draggedItem={draggedItem}
|
draggedItem={draggedItem}
|
||||||
dragOverInfo={dragOverInfo}
|
dragOverInfo={dragOverInfo}
|
||||||
onRemoveSound={onRemoveSound}
|
onRemoveSound={onRemoveSound}
|
||||||
|
timeIntervals={timeIntervals}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -79,14 +79,58 @@ export function SequencerPage() {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Helper function to snap time to 100ms intervals with improved precision
|
// Calculate logical time intervals based on zoom level (shared with SequencerCanvas)
|
||||||
const snapToGrid = useCallback((timeInSeconds: number): number => {
|
const getTimeIntervals = useCallback((zoom: number, duration: number) => {
|
||||||
const snapIntervalMs = 100 // 100ms snap interval
|
const durationSeconds = duration / 1000
|
||||||
const timeInMs = Math.max(0, timeInSeconds * 1000) // Ensure non-negative
|
const minorIntervals: number[] = []
|
||||||
const snappedMs = Math.round(timeInMs / snapIntervalMs) * snapIntervalMs
|
const majorIntervals: number[] = []
|
||||||
return Math.max(0, snappedMs / 1000) // Convert back to seconds, ensure non-negative
|
|
||||||
|
// 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
|
// Update drag over info based on current mouse position and over target
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (draggedItem && currentMousePos && (draggedItem.type === 'sound' || draggedItem.type === 'placed-sound')) {
|
if (draggedItem && currentMousePos && (draggedItem.type === 'sound' || draggedItem.type === 'placed-sound')) {
|
||||||
@@ -102,9 +146,9 @@ export function SequencerPage() {
|
|||||||
currentMousePos.y <= rect.bottom
|
currentMousePos.y <= rect.bottom
|
||||||
) {
|
) {
|
||||||
const rawX = currentMousePos.x - rect.left
|
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 rawTimeSeconds = rawX / state.zoom
|
||||||
const snappedTimeSeconds = snapToGrid(rawTimeSeconds)
|
const snappedTimeSeconds = snapToGrid(rawTimeSeconds, state.zoom, state.duration)
|
||||||
const snappedX = snappedTimeSeconds * state.zoom
|
const snappedX = snappedTimeSeconds * state.zoom
|
||||||
setDragOverInfo({ trackId: track.id, x: Math.max(0, snappedX) })
|
setDragOverInfo({ trackId: track.id, x: Math.max(0, snappedX) })
|
||||||
return
|
return
|
||||||
@@ -116,7 +160,7 @@ export function SequencerPage() {
|
|||||||
} else {
|
} else {
|
||||||
setDragOverInfo(null)
|
setDragOverInfo(null)
|
||||||
}
|
}
|
||||||
}, [draggedItem, currentMousePos, state.tracks, state.zoom, snapToGrid])
|
}, [draggedItem, currentMousePos, state.tracks, state.zoom, state.duration, snapToGrid])
|
||||||
|
|
||||||
const handleDragEnd = useCallback((event: DragEndEvent) => {
|
const handleDragEnd = useCallback((event: DragEndEvent) => {
|
||||||
const { active, over } = event
|
const { active, over } = event
|
||||||
@@ -380,6 +424,7 @@ export function SequencerPage() {
|
|||||||
draggedItem={draggedItem}
|
draggedItem={draggedItem}
|
||||||
dragOverInfo={dragOverInfo}
|
dragOverInfo={dragOverInfo}
|
||||||
onRemoveSound={handleRemoveSound}
|
onRemoveSound={handleRemoveSound}
|
||||||
|
timeIntervals={getTimeIntervals(state.zoom, state.duration)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user