diff options
| author | Maze <167211895+mazeincoding@users.noreply.github.com> | 2026-01-31 00:20:04 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-01-31 00:20:04 +0100 |
| commit | 7bf0984698e43b9aa2dd879f65951318ed410390 (patch) | |
| tree | ccf2cd590a47d550152afc07129cb55c71a9ec35 /apps/web/src/hooks/timeline/use-timeline-snapping.ts | |
| parent | 0173db99448d1c4c6415dcf15272e17551b48b04 (diff) | |
| download | opencut-7bf0984698e43b9aa2dd879f65951318ed410390.tar.gz | |
codebase overhaul (#697)
Diffstat (limited to 'apps/web/src/hooks/timeline/use-timeline-snapping.ts')
| -rw-r--r-- | apps/web/src/hooks/timeline/use-timeline-snapping.ts | 159 |
1 files changed, 159 insertions, 0 deletions
diff --git a/apps/web/src/hooks/timeline/use-timeline-snapping.ts b/apps/web/src/hooks/timeline/use-timeline-snapping.ts new file mode 100644 index 00000000..4cd6f775 --- /dev/null +++ b/apps/web/src/hooks/timeline/use-timeline-snapping.ts @@ -0,0 +1,159 @@ +import { useCallback } from "react"; +import type { TimelineTrack } from "@/types/timeline"; +import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants"; + +export interface SnapPoint { + time: number; + type: "element-start" | "element-end" | "playhead"; + elementId?: string; + trackId?: string; +} + +export interface SnapResult { + snappedTime: number; + snapPoint: SnapPoint | null; + snapDistance: number; +} + +export interface UseTimelineSnappingOptions { + snapThreshold?: number; + enableElementSnapping?: boolean; + enablePlayheadSnapping?: boolean; +} + +export function useTimelineSnapping({ + snapThreshold = 10, + enableElementSnapping = true, + enablePlayheadSnapping = true, +}: UseTimelineSnappingOptions = {}) { + const findSnapPoints = useCallback( + ({ + tracks, + playheadTime, + excludeElementId, + }: { + tracks: Array<TimelineTrack>; + playheadTime: number; + excludeElementId?: string; + }): SnapPoint[] => { + const snapPoints: SnapPoint[] = []; + + if (enableElementSnapping) { + for (const track of tracks) { + for (const element of track.elements) { + if (element.id === excludeElementId) continue; + + const elementStart = element.startTime; + const elementEnd = element.startTime + element.duration; + + snapPoints.push( + { + time: elementStart, + type: "element-start", + elementId: element.id, + trackId: track.id, + }, + { + time: elementEnd, + type: "element-end", + elementId: element.id, + trackId: track.id, + }, + ); + } + } + } + + if (enablePlayheadSnapping) { + snapPoints.push({ + time: playheadTime, + type: "playhead", + }); + } + + return snapPoints; + }, + [enableElementSnapping, enablePlayheadSnapping], + ); + + const snapToNearestPoint = useCallback( + ({ + targetTime, + snapPoints, + zoomLevel, + }: { + targetTime: number; + snapPoints: Array<SnapPoint>; + zoomLevel: number; + }): SnapResult => { + const pixelsPerSecond = TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel; + const thresholdInSeconds = snapThreshold / pixelsPerSecond; + + let closestSnapPoint: SnapPoint | null = null; + let closestDistance = Infinity; + + for (const snapPoint of snapPoints) { + const distance = Math.abs(targetTime - snapPoint.time); + if (distance < thresholdInSeconds && distance < closestDistance) { + closestDistance = distance; + closestSnapPoint = snapPoint; + } + } + + return { + snappedTime: closestSnapPoint ? closestSnapPoint.time : targetTime, + snapPoint: closestSnapPoint, + snapDistance: closestDistance, + }; + }, + [snapThreshold], + ); + + const snapElementEdge = useCallback( + ({ + targetTime, + elementDuration, + tracks, + playheadTime, + zoomLevel, + excludeElementId, + snapToStart = true, + }: { + targetTime: number; + elementDuration: number; + tracks: Array<TimelineTrack>; + playheadTime: number; + zoomLevel: number; + excludeElementId?: string; + snapToStart?: boolean; + }): SnapResult => { + const snapPoints = findSnapPoints({ + tracks, + playheadTime, + excludeElementId, + }); + + const effectiveTargetTime = snapToStart + ? targetTime + : targetTime + elementDuration; + const snapResult = snapToNearestPoint({ + targetTime: effectiveTargetTime, + snapPoints, + zoomLevel, + }); + + if (!snapToStart && snapResult.snapPoint) { + snapResult.snappedTime = snapResult.snappedTime - elementDuration; + } + + return snapResult; + }, + [findSnapPoints, snapToNearestPoint], + ); + + return { + snapElementEdge, + findSnapPoints, + snapToNearestPoint, + }; +} |
