feat: implement time snapping to 100ms intervals for improved sound placement accuracy
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user