diff options
Diffstat (limited to 'apps/web/src/hooks/timeline/element')
3 files changed, 45 insertions, 38 deletions
diff --git a/apps/web/src/hooks/timeline/element/use-element-interaction.ts b/apps/web/src/hooks/timeline/element/use-element-interaction.ts index 71ae4704..6fcd4168 100644 --- a/apps/web/src/hooks/timeline/element/use-element-interaction.ts +++ b/apps/web/src/hooks/timeline/element/use-element-interaction.ts @@ -10,8 +10,9 @@ import { useEditor } from "@/hooks/use-editor"; import { useShiftKey } from "@/hooks/use-shift-key"; import { useElementSelection } from "@/hooks/timeline/element/use-element-selection"; import { BASE_TIMELINE_PIXELS_PER_SECOND } from "@/lib/timeline/scale"; +import { TICKS_PER_SECOND } from "@/lib/wasm"; import { TIMELINE_DRAG_THRESHOLD_PX } from "@/components/editor/panels/timeline/interaction"; -import { snapTimeToFrame } from "opencut-wasm"; +import { roundToFrame } from "opencut-wasm"; import { computeDropTarget } from "@/components/editor/panels/timeline/drop-target"; import { getMouseTimeFromClientX } from "@/lib/timeline/drag-utils"; import { generateUUID } from "@/utils/id"; @@ -67,7 +68,8 @@ function getClickOffsetTime({ zoomLevel: number; }): number { const clickOffsetX = clientX - elementRect.left; - return clickOffsetX / (BASE_TIMELINE_PIXELS_PER_SECOND * zoomLevel); + const seconds = clickOffsetX / (BASE_TIMELINE_PIXELS_PER_SECOND * zoomLevel); + return Math.round(seconds * TICKS_PER_SECOND); } function getVerticalDragDirection({ @@ -297,15 +299,17 @@ export function useElementInteraction({ zoomLevel, scrollLeft, }); - const adjustedTime = - mouseTime - pendingDragRef.current.clickOffsetTime; - const snappedTime = snapTimeToFrame({ - time: adjustedTime, - fps: activeProject.settings.fps, - }); - startDrag({ - ...pendingDragRef.current, - initialCurrentTime: snappedTime, + const adjustedTime = Math.max( + 0, + mouseTime - pendingDragRef.current.clickOffsetTime, + ); + const snappedTime = roundToFrame({ + time: adjustedTime, + rate: activeProject.settings.fps, + }) ?? adjustedTime; + startDrag({ + ...pendingDragRef.current, + initialCurrentTime: snappedTime, initialCurrentMouseY: clientY, }); startedDragThisEvent = true; @@ -343,9 +347,9 @@ export function useElementInteraction({ zoomLevel, scrollLeft, }); - const adjustedTime = mouseTime - dragState.clickOffsetTime; + const adjustedTime = Math.max(0, mouseTime - dragState.clickOffsetTime); const fps = activeProject.settings.fps; - const frameSnappedTime = snapTimeToFrame({ time: adjustedTime, fps }); + const frameSnappedTime = roundToFrame({ time: adjustedTime, rate: fps }) ?? adjustedTime; const sourceTrack = tracks.find(({ id }) => id === dragState.trackId); const movingElement = sourceTrack?.elements.find( diff --git a/apps/web/src/hooks/timeline/element/use-element-resize.ts b/apps/web/src/hooks/timeline/element/use-element-resize.ts index 3dd98bea..dec87eef 100644 --- a/apps/web/src/hooks/timeline/element/use-element-resize.ts +++ b/apps/web/src/hooks/timeline/element/use-element-resize.ts @@ -1,6 +1,7 @@ import { useState, useEffect, useRef, useCallback } from "react"; import { BASE_TIMELINE_PIXELS_PER_SECOND } from "@/lib/timeline/scale"; -import { snapTimeToFrame } from "opencut-wasm"; +import { TICKS_PER_SECOND } from "@/lib/wasm"; +import { roundToFrame } from "opencut-wasm"; import type { TimelineElement, TimelineTrack } from "@/lib/timeline"; import { useEditor } from "@/hooks/use-editor"; import { useShiftKey } from "@/hooks/use-shift-key"; @@ -185,13 +186,14 @@ export function useTimelineElementResize({ ({ clientX }: { clientX: number }) => { if (!resizing) return; - const deltaX = clientX - resizing.startX; - let deltaTime = - deltaX / (BASE_TIMELINE_PIXELS_PER_SECOND * zoomLevel); - let resizeSnapPoint: SnapPoint | null = null; + const deltaX = clientX - resizing.startX; + let deltaTime = Math.round( + (deltaX / (BASE_TIMELINE_PIXELS_PER_SECOND * zoomLevel)) * TICKS_PER_SECOND, + ); + let resizeSnapPoint: SnapPoint | null = null; - const projectFps = editor.project.getActive().settings.fps; - const minDurationSeconds = 1 / projectFps; + const projectFps = editor.project.getActive().settings.fps; + const minDuration = Math.round(TICKS_PER_SECOND * projectFps.denominator / projectFps.numerator); const shouldSnap = snappingEnabled && !isShiftHeldRef.current; if (shouldSnap) { const tracks = editor.timeline.getTracks(); @@ -277,19 +279,19 @@ export function useTimelineElementResize({ const maxAllowed = sourceDuration - resizing.initialTrimEnd - - getVisibleSourceSpanForDuration(minDurationSeconds); + getVisibleSourceSpanForDuration(minDuration); const calculated = resizing.initialTrimStart + getSourceDeltaForClipDelta(deltaTime); if (calculated >= 0 && calculated <= maxAllowed) { - const newTrimStart = snapTimeToFrame({ time: Math.min(maxAllowed, Math.max(minTrimStartForNeighbor, calculated)), fps: projectFps }); + const newTrimStart = roundToFrame({ time: Math.min(maxAllowed, Math.max(minTrimStartForNeighbor, calculated)), rate: projectFps }) ?? Math.min(maxAllowed, Math.max(minTrimStartForNeighbor, calculated)); const visibleSourceSpan = Math.max( 0, sourceDuration - newTrimStart - resizing.initialTrimEnd, ); - const newDuration = snapTimeToFrame({ time: getDurationForVisibleSourceSpan(visibleSourceSpan), fps: projectFps }); + const newDuration = roundToFrame({ time: getDurationForVisibleSourceSpan(visibleSourceSpan), rate: projectFps }) ?? getDurationForVisibleSourceSpan(visibleSourceSpan); const trimDelta = resizing.initialDuration - newDuration; - const newStartTime = snapTimeToFrame({ time: resizing.initialStartTime + trimDelta, fps: projectFps }); + const newStartTime = roundToFrame({ time: resizing.initialStartTime + trimDelta, rate: projectFps }) ?? resizing.initialStartTime + trimDelta; setCurrentTrimStart(newTrimStart); setCurrentStartTime(newStartTime); @@ -311,8 +313,8 @@ export function useTimelineElementResize({ ) : Math.min(extensionAmount, maxExtension), ); - const newStartTime = snapTimeToFrame({ time: resizing.initialStartTime - actualExtension, fps: projectFps }); - const newDuration = snapTimeToFrame({ time: resizing.initialDuration + actualExtension, fps: projectFps }); + const newStartTime = roundToFrame({ time: resizing.initialStartTime - actualExtension, rate: projectFps }) ?? resizing.initialStartTime - actualExtension; + const newDuration = roundToFrame({ time: resizing.initialDuration + actualExtension, rate: projectFps }) ?? resizing.initialDuration + actualExtension; setCurrentTrimStart(0); setCurrentStartTime(newStartTime); @@ -338,8 +340,8 @@ export function useTimelineElementResize({ 0, sourceDuration - newTrimStart - resizing.initialTrimEnd, ); - const newDuration = snapTimeToFrame({ time: getDurationForVisibleSourceSpan(visibleSourceSpan), fps: projectFps }); - const newStartTime = snapTimeToFrame({ time: resizing.initialStartTime + (resizing.initialDuration - newDuration), fps: projectFps }); + const newDuration = roundToFrame({ time: getDurationForVisibleSourceSpan(visibleSourceSpan), rate: projectFps }) ?? getDurationForVisibleSourceSpan(visibleSourceSpan); + const newStartTime = roundToFrame({ time: resizing.initialStartTime + (resizing.initialDuration - newDuration), rate: projectFps }) ?? resizing.initialStartTime + (resizing.initialDuration - newDuration); setCurrentTrimStart(newTrimStart); setCurrentStartTime(newStartTime); @@ -366,7 +368,7 @@ export function useTimelineElementResize({ const extensionNeeded = Math.abs(newTrimEnd); const baseDuration = resizing.initialDuration + resizing.initialTrimEnd; - const newDuration = snapTimeToFrame({ time: Math.min(baseDuration + extensionNeeded, maxAllowedDuration), fps: projectFps }); + const newDuration = roundToFrame({ time: Math.min(baseDuration + extensionNeeded, maxAllowedDuration), rate: projectFps }) ?? Math.min(baseDuration + extensionNeeded, maxAllowedDuration); setCurrentDuration(newDuration); setCurrentTrimEnd(0); @@ -376,7 +378,7 @@ export function useTimelineElementResize({ const unclampedDuration = getDurationForVisibleSourceSpan( Math.max(0, sourceDuration - resizing.initialTrimStart), ); - const newDuration = snapTimeToFrame({ time: Math.min(unclampedDuration, maxAllowedDuration), fps: projectFps }); + const newDuration = roundToFrame({ time: Math.min(unclampedDuration, maxAllowedDuration), rate: projectFps }) ?? Math.min(unclampedDuration, maxAllowedDuration); setCurrentDuration(newDuration); setCurrentTrimEnd(0); @@ -395,17 +397,17 @@ export function useTimelineElementResize({ const maxTrimEnd = sourceDuration - resizing.initialTrimStart - - getVisibleSourceSpanForDuration(minDurationSeconds); + getVisibleSourceSpanForDuration(minDuration); const clampedTrimEnd = Math.min( maxTrimEnd, Math.max(minTrimEndForNeighbor, newTrimEnd), ); - const finalTrimEnd = snapTimeToFrame({ time: clampedTrimEnd, fps: projectFps }); + const finalTrimEnd = roundToFrame({ time: clampedTrimEnd, rate: projectFps }) ?? clampedTrimEnd; const visibleSourceSpan = Math.max( 0, sourceDuration - resizing.initialTrimStart - finalTrimEnd, ); - const newDuration = snapTimeToFrame({ time: getDurationForVisibleSourceSpan(visibleSourceSpan), fps: projectFps }); + const newDuration = roundToFrame({ time: getDurationForVisibleSourceSpan(visibleSourceSpan), rate: projectFps }) ?? getDurationForVisibleSourceSpan(visibleSourceSpan); setCurrentTrimEnd(finalTrimEnd); setCurrentDuration(newDuration); diff --git a/apps/web/src/hooks/timeline/element/use-keyframe-drag.ts b/apps/web/src/hooks/timeline/element/use-keyframe-drag.ts index b66bbea8..24bff26a 100644 --- a/apps/web/src/hooks/timeline/element/use-keyframe-drag.ts +++ b/apps/web/src/hooks/timeline/element/use-keyframe-drag.ts @@ -8,9 +8,10 @@ import { import { useEditor } from "@/hooks/use-editor"; import { getKeyframeById } from "@/lib/animation"; import { useKeyframeSelection } from "./use-keyframe-selection"; -import { snapTimeToFrame, getSnappedSeekTime } from "opencut-wasm"; +import { roundToFrame, snappedSeekTime } from "opencut-wasm"; import { timelineTimeToSnappedPixels } from "@/lib/timeline"; import { BASE_TIMELINE_PIXELS_PER_SECOND } from "@/lib/timeline/scale"; +import { TICKS_PER_SECOND } from "@/lib/wasm"; import { TIMELINE_DRAG_THRESHOLD_PX } from "@/components/editor/panels/timeline/interaction"; import { RetimeKeyframeCommand } from "@/lib/commands/timeline/element/keyframes/retime-keyframe"; import { BatchCommand } from "@/lib/commands"; @@ -144,9 +145,9 @@ export function useKeyframeDrag({ if (!dragState.isDragging) return; - const startX = mouseDownXRef.current ?? clientX; - const rawDelta = (clientX - startX) / pixelsPerSecond; - const snappedDelta = snapTimeToFrame({ time: rawDelta, fps }); + const startX = mouseDownXRef.current ?? clientX; + const rawDelta = Math.round(((clientX - startX) / pixelsPerSecond) * TICKS_PER_SECOND); + const snappedDelta = roundToFrame({ time: rawDelta, rate: fps }) ?? rawDelta; setDragState((previous) => ({ ...previous, deltaTime: snappedDelta })); }; @@ -255,7 +256,7 @@ export function useKeyframeDrag({ if (wasDrag) return; const duration = editor.timeline.getTotalDuration(); - const seekTime = getSnappedSeekTime({ rawTime: displayedStartTime + indicatorTime, duration, fps }); + const seekTime = snappedSeekTime({ time: displayedStartTime + indicatorTime, duration, rate: fps }) ?? displayedStartTime + indicatorTime; editor.playback.seek({ time: seekTime }); if (event.shiftKey) { |
