feat: implement 100ms snapping for sound placement and enhance zoom controls in Sequencer
This commit is contained in:
@@ -2,7 +2,7 @@ import { useDroppable, useDraggable } from '@dnd-kit/core'
|
||||
import type { Track, PlacedSound } from '@/pages/SequencerPage'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Trash2, Volume2 } from 'lucide-react'
|
||||
import { useState, forwardRef, useRef, useEffect } from 'react'
|
||||
import { forwardRef, useRef, useEffect } from 'react'
|
||||
|
||||
interface SequencerCanvasProps {
|
||||
tracks: Track[]
|
||||
@@ -15,6 +15,9 @@ interface SequencerCanvasProps {
|
||||
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}
|
||||
onZoomChange?: (newZoom: number, mouseX?: number) => void
|
||||
minZoom?: number
|
||||
maxZoom?: number
|
||||
}
|
||||
|
||||
interface TrackRowProps {
|
||||
@@ -34,10 +37,9 @@ interface PlacedSoundItemProps {
|
||||
zoom: number
|
||||
trackId: string
|
||||
onRemove: (soundId: string) => void
|
||||
minorInterval: number
|
||||
}
|
||||
|
||||
function PlacedSoundItem({ sound, zoom, trackId, onRemove, minorInterval }: PlacedSoundItemProps) {
|
||||
function PlacedSoundItem({ sound, zoom, trackId, onRemove }: PlacedSoundItemProps) {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
@@ -58,9 +60,10 @@ function PlacedSoundItem({ sound, zoom, trackId, onRemove, minorInterval }: Plac
|
||||
} : undefined
|
||||
|
||||
const width = (sound.duration / 1000) * zoom // Convert ms to seconds for zoom calculation
|
||||
// Ensure placed sounds are positioned at snapped locations using current minor interval
|
||||
// Ensure placed sounds are positioned at 100ms snapped locations
|
||||
const startTimeSeconds = sound.startTime / 1000
|
||||
const snappedStartTime = Math.round(startTimeSeconds / minorInterval) * minorInterval
|
||||
const snapIntervalMs = 100 // 100ms snap interval
|
||||
const snappedStartTime = Math.round((startTimeSeconds * 1000) / snapIntervalMs) * snapIntervalMs / 1000
|
||||
const left = Math.max(0, snappedStartTime) * zoom
|
||||
|
||||
const formatTime = (seconds: number): string => {
|
||||
@@ -218,7 +221,6 @@ function TrackRow({ track, duration, zoom, isPlaying, currentTime, draggedItem,
|
||||
zoom={zoom}
|
||||
trackId={track.id}
|
||||
onRemove={handleRemoveSound}
|
||||
minorInterval={timeIntervals.minorInterval}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -237,6 +239,9 @@ export const SequencerCanvas = forwardRef<HTMLDivElement, SequencerCanvasProps>(
|
||||
dragOverInfo,
|
||||
onRemoveSound,
|
||||
timeIntervals,
|
||||
onZoomChange,
|
||||
minZoom = 10,
|
||||
maxZoom = 200,
|
||||
}, ref) => {
|
||||
const totalWidth = (duration / 1000) * zoom // Convert ms to seconds for zoom calculation
|
||||
const timelineRef = useRef<HTMLDivElement>(null)
|
||||
@@ -264,6 +269,54 @@ export const SequencerCanvas = forwardRef<HTMLDivElement, SequencerCanvasProps>(
|
||||
onScroll?.()
|
||||
}
|
||||
|
||||
// Handle mouse wheel zoom with Ctrl key using native event listeners
|
||||
useEffect(() => {
|
||||
if (!onZoomChange) return
|
||||
|
||||
const handleWheel = (e: WheelEvent) => {
|
||||
if (!e.ctrlKey) return
|
||||
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
// Use the same discrete steps as the zoom buttons (+5/-5)
|
||||
const zoomStep = 5
|
||||
const delta = e.deltaY > 0 ? -zoomStep : zoomStep // Inverted for natural feel
|
||||
const newZoom = Math.min(Math.max(zoom + delta, minZoom), maxZoom)
|
||||
|
||||
if (newZoom !== zoom) {
|
||||
// Get mouse position relative to the scrollable content
|
||||
const target = e.target as HTMLElement
|
||||
const scrollContainer = target.closest('[data-scroll-container]') as HTMLElement
|
||||
if (scrollContainer) {
|
||||
const rect = scrollContainer.getBoundingClientRect()
|
||||
const mouseX = e.clientX - rect.left + scrollContainer.scrollLeft
|
||||
onZoomChange(newZoom, mouseX)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add wheel event listeners to both timeline and tracks
|
||||
const timelineElement = timelineRef.current
|
||||
const tracksElement = ref && typeof ref === 'object' && ref.current ? ref.current : null
|
||||
|
||||
if (timelineElement) {
|
||||
timelineElement.addEventListener('wheel', handleWheel, { passive: false })
|
||||
}
|
||||
if (tracksElement) {
|
||||
tracksElement.addEventListener('wheel', handleWheel, { passive: false })
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (timelineElement) {
|
||||
timelineElement.removeEventListener('wheel', handleWheel)
|
||||
}
|
||||
if (tracksElement) {
|
||||
tracksElement.removeEventListener('wheel', handleWheel)
|
||||
}
|
||||
}
|
||||
}, [onZoomChange, zoom, minZoom, maxZoom, ref])
|
||||
|
||||
return (
|
||||
<div ref={setCanvasDropRef} className="h-full flex flex-col overflow-hidden">
|
||||
{/* Time ruler */}
|
||||
@@ -275,6 +328,7 @@ export const SequencerCanvas = forwardRef<HTMLDivElement, SequencerCanvasProps>(
|
||||
scrollbarWidth: 'none',
|
||||
msOverflowStyle: 'none'
|
||||
}}
|
||||
data-scroll-container
|
||||
>
|
||||
<div className="relative h-full" style={{ width: `${totalWidth}px` }}>
|
||||
{/* Minor time markers */}
|
||||
@@ -326,6 +380,7 @@ export const SequencerCanvas = forwardRef<HTMLDivElement, SequencerCanvasProps>(
|
||||
ref={ref}
|
||||
className="flex-1 overflow-auto"
|
||||
onScroll={handleTracksScroll}
|
||||
data-scroll-container
|
||||
>
|
||||
<div style={{ width: `${totalWidth}px`, paddingBottom: '52px' }}>
|
||||
{tracks.map((track) => (
|
||||
|
||||
@@ -53,11 +53,11 @@ export function TimelineControls({
|
||||
}
|
||||
|
||||
const increaseZoom = () => {
|
||||
onZoomChange(Math.min(maxZoom, zoom + 10))
|
||||
onZoomChange(Math.min(maxZoom, zoom + 5))
|
||||
}
|
||||
|
||||
const decreaseZoom = () => {
|
||||
onZoomChange(Math.max(minZoom, zoom - 10))
|
||||
onZoomChange(Math.max(minZoom, zoom - 5))
|
||||
}
|
||||
|
||||
const formatTime = (seconds: number): string => {
|
||||
|
||||
Reference in New Issue
Block a user