This commit is contained in:
Maze Winther
2025-12-25 21:15:36 +01:00
parent 2da84e7132
commit d5ca991501
102 changed files with 16028 additions and 6675 deletions
@@ -0,0 +1,156 @@
import { useCallback } from "react";
import { 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; // Distance in pixels to trigger snapping
enableElementSnapping?: boolean;
enablePlayheadSnapping?: boolean;
}
export function useTimelineSnapping({
snapThreshold = 10,
enableElementSnapping = true,
enablePlayheadSnapping = true,
}: UseTimelineSnappingOptions = {}) {
const findSnapPoints = useCallback(
(
tracks: TimelineTrack[],
currentTime: number,
playheadTime: number,
zoomLevel: number,
excludeElementId?: string,
): SnapPoint[] => {
const snapPoints: SnapPoint[] = [];
// Add element snap points
if (enableElementSnapping) {
tracks.forEach((track) => {
track.elements.forEach((element) => {
// Skip the element being dragged
if (element.id === excludeElementId) return;
const elementStart = element.startTime;
const elementEnd =
element.startTime +
(element.duration - element.trimStart - element.trimEnd);
snapPoints.push(
{
time: elementStart,
type: "element-start",
elementId: element.id,
trackId: track.id,
},
{
time: elementEnd,
type: "element-end",
elementId: element.id,
trackId: track.id,
},
);
});
});
}
// Add playhead snap point
if (enablePlayheadSnapping) {
snapPoints.push({
time: playheadTime,
type: "playhead",
});
}
return snapPoints;
},
[enableElementSnapping, enablePlayheadSnapping],
);
const snapToNearestPoint = useCallback(
(
targetTime: number,
snapPoints: SnapPoint[],
zoomLevel: number,
): SnapResult => {
const pixelsPerSecond = TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel;
const thresholdInSeconds = snapThreshold / pixelsPerSecond;
let closestSnapPoint: SnapPoint | null = null;
let closestDistance = Infinity;
snapPoints.forEach((snapPoint) => {
const distance = Math.abs(targetTime - snapPoint.time);
if (distance < thresholdInSeconds && distance < closestDistance) {
closestDistance = distance;
closestSnapPoint = snapPoint;
}
});
return {
snappedTime: closestSnapPoint
? (closestSnapPoint as SnapPoint).time
: targetTime,
snapPoint: closestSnapPoint,
snapDistance: closestDistance,
};
},
[snapThreshold],
);
const snapElementEdge = useCallback(
(
targetTime: number,
elementDuration: number,
tracks: TimelineTrack[],
playheadTime: number,
zoomLevel: number,
excludeElementId?: string,
snapToStart = true, // true for start edge, false for end edge
): SnapResult => {
const snapPoints = findSnapPoints(
tracks,
targetTime,
playheadTime,
zoomLevel,
excludeElementId,
);
// For end edge snapping, we need to account for element duration
const effectiveTargetTime = snapToStart
? targetTime
: targetTime + elementDuration;
const snapResult = snapToNearestPoint(
effectiveTargetTime,
snapPoints,
zoomLevel,
);
// Adjust the snapped time back for end edge
if (!snapToStart && snapResult.snapPoint) {
snapResult.snappedTime = snapResult.snappedTime - elementDuration;
}
return snapResult;
},
[findSnapPoints, snapToNearestPoint],
);
return {
snapElementEdge,
findSnapPoints,
snapToNearestPoint,
};
}