feat: implement time snapping to 100ms intervals for improved sound placement accuracy

This commit is contained in:
JSC
2025-09-03 21:03:28 +02:00
parent d4b87aafe3
commit cd7af24831
2 changed files with 41 additions and 12 deletions

View File

@@ -54,8 +54,18 @@ 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
const left = (sound.startTime / 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
const formatTime = (seconds: number): string => { const formatTime = (seconds: number): string => {
const mins = Math.floor(seconds / 60) const mins = Math.floor(seconds / 60)
@@ -110,7 +120,6 @@ 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 }: TrackRowProps) {
const totalWidth = (duration / 1000) * zoom // Convert ms to seconds for zoom calculation
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({
@@ -203,12 +212,14 @@ function TrackRow({ track, duration, zoom, isPlaying, currentTime, draggedItem,
))} ))}
</div> </div>
{/* Precise drag preview */} {/* Precise drag preview (dragOverInfo.x is already snapped) */}
{draggedItem && dragOverInfo && dragOverInfo.trackId === track.id && (() => { {draggedItem && dragOverInfo && dragOverInfo.trackId === track.id && (() => {
const soundDurationMs = draggedItem.type === 'sound' const soundDurationMs = draggedItem.type === 'sound'
? draggedItem.sound.duration // Already in ms ? draggedItem.sound.duration // Already in ms
: draggedItem.duration // Already in ms : draggedItem.duration // Already in ms
const soundDurationSeconds = soundDurationMs / 1000 const soundDurationSeconds = soundDurationMs / 1000
// dragOverInfo.x is already snapped in the parent component
const startTimeSeconds = dragOverInfo.x / zoom const startTimeSeconds = dragOverInfo.x / zoom
const endTimeSeconds = startTimeSeconds + soundDurationSeconds const endTimeSeconds = startTimeSeconds + soundDurationSeconds
const durationSeconds = duration / 1000 const durationSeconds = duration / 1000

View File

@@ -79,6 +79,14 @@ 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
}, [])
// 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')) {
@@ -93,8 +101,12 @@ export function SequencerPage() {
currentMousePos.y >= rect.top && currentMousePos.y >= rect.top &&
currentMousePos.y <= rect.bottom currentMousePos.y <= rect.bottom
) { ) {
const x = currentMousePos.x - rect.left const rawX = currentMousePos.x - rect.left
setDragOverInfo({ trackId: track.id, x: Math.max(0, x) }) // Apply snapping to the drag over position for consistency
const rawTimeSeconds = rawX / state.zoom
const snappedTimeSeconds = snapToGrid(rawTimeSeconds)
const snappedX = snappedTimeSeconds * state.zoom
setDragOverInfo({ trackId: track.id, x: Math.max(0, snappedX) })
return return
} }
} }
@@ -104,7 +116,7 @@ export function SequencerPage() {
} else { } else {
setDragOverInfo(null) setDragOverInfo(null)
} }
}, [draggedItem, currentMousePos, state.tracks]) }, [draggedItem, currentMousePos, state.tracks, state.zoom, snapToGrid])
const handleDragEnd = useCallback((event: DragEndEvent) => { const handleDragEnd = useCallback((event: DragEndEvent) => {
const { active, over } = event const { active, over } = event
@@ -113,10 +125,10 @@ export function SequencerPage() {
// Handle sound drop from library to track // Handle sound drop from library to track
if (dragData?.type === 'sound' && overData?.type === 'track') { if (dragData?.type === 'sound' && overData?.type === 'track') {
// Use precise drop position if available (convert from pixels to milliseconds) // Use precise drop position if available (dragOverInfo.x is already snapped)
let startTime = 0 let startTime = 0
if (dragOverInfo && dragOverInfo.trackId === overData.trackId) { if (dragOverInfo && dragOverInfo.trackId === overData.trackId) {
startTime = Math.max(0, (dragOverInfo.x / state.zoom) * 1000) // Convert seconds to milliseconds startTime = Math.max(0, (dragOverInfo.x / state.zoom) * 1000) // Convert to milliseconds
} }
const soundDuration = dragData.sound.duration // Already in milliseconds const soundDuration = dragData.sound.duration // Already in milliseconds
@@ -149,10 +161,10 @@ export function SequencerPage() {
// Handle moving placed sounds within tracks // Handle moving placed sounds within tracks
if (dragData?.type === 'placed-sound' && overData?.type === 'track') { if (dragData?.type === 'placed-sound' && overData?.type === 'track') {
// Use precise drop position if available (convert from pixels to milliseconds) // Use precise drop position if available (dragOverInfo.x is already snapped)
let startTime = dragData.startTime || 0 let startTime = dragData.startTime || 0
if (dragOverInfo && dragOverInfo.trackId === overData.trackId) { if (dragOverInfo && dragOverInfo.trackId === overData.trackId) {
startTime = Math.max(0, (dragOverInfo.x / state.zoom) * 1000) // Convert seconds to milliseconds startTime = Math.max(0, (dragOverInfo.x / state.zoom) * 1000) // Convert to milliseconds
} }
// Restrict placement to within track duration (all in milliseconds) // Restrict placement to within track duration (all in milliseconds)
@@ -170,7 +182,10 @@ export function SequencerPage() {
if (track.id === sourceTrackId && sourceTrackId === targetTrackId) { if (track.id === sourceTrackId && sourceTrackId === targetTrackId) {
// Moving within the same track - just update position // Moving within the same track - just update position
const updatedSound: PlacedSound = { const updatedSound: PlacedSound = {
...dragData, id: dragData.id,
soundId: dragData.soundId,
name: dragData.name,
duration: dragData.duration,
startTime, startTime,
trackId: targetTrackId, trackId: targetTrackId,
} }
@@ -189,7 +204,10 @@ export function SequencerPage() {
} else if (track.id === targetTrackId) { } else if (track.id === targetTrackId) {
// Add to target track (different track move) // Add to target track (different track move)
const updatedSound: PlacedSound = { const updatedSound: PlacedSound = {
...dragData, id: dragData.id,
soundId: dragData.soundId,
name: dragData.name,
duration: dragData.duration,
startTime, startTime,
trackId: targetTrackId, trackId: targetTrackId,
} }