diff options
| author | Maze Winther <mazewinther@gmail.com> | 2025-06-25 19:51:50 +0200 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2025-06-25 19:51:50 +0200 |
| commit | baa5c43e7c9b8e6415179f19d104c958553985aa (patch) | |
| tree | a380948333ea036fb0bb9eeb15cfb37dea57cfc4 /apps/web/src | |
| parent | bf64ec11334c39fde04c09b44460a489bcab1f2d (diff) | |
| download | opencut-baa5c43e7c9b8e6415179f19d104c958553985aa.tar.gz | |
feat: improve clips dragging experience
Diffstat (limited to 'apps/web/src')
| -rw-r--r-- | apps/web/src/components/editor/timeline.tsx | 70 |
1 files changed, 32 insertions, 38 deletions
diff --git a/apps/web/src/components/editor/timeline.tsx b/apps/web/src/components/editor/timeline.tsx index 1ecbe545..584a45f7 100644 --- a/apps/web/src/components/editor/timeline.tsx +++ b/apps/web/src/components/editor/timeline.tsx @@ -1241,7 +1241,18 @@ function TimelineTrackContent({ };
const handleClipDragStart = (e: React.DragEvent, clip: any) => {
- const dragData = { clipId: clip.id, trackId: track.id, name: clip.name };
+ // Calculate the offset from the left edge of the clip to where the user clicked
+ const clipElement = e.currentTarget.parentElement as HTMLElement;
+ const clipRect = clipElement.getBoundingClientRect();
+ const clickOffsetX = e.clientX - clipRect.left;
+ const clickOffsetTime = clickOffsetX / (50 * zoomLevel);
+
+ const dragData = {
+ clipId: clip.id,
+ trackId: track.id,
+ name: clip.name,
+ clickOffsetTime: clickOffsetTime,
+ };
e.dataTransfer.setData(
"application/x-timeline-clip",
@@ -1467,7 +1478,11 @@ function TimelineTrackContent({ );
if (!timelineClipData) return;
- const { clipId, trackId: fromTrackId } = JSON.parse(timelineClipData);
+ const {
+ clipId,
+ trackId: fromTrackId,
+ clickOffsetTime = 0,
+ } = JSON.parse(timelineClipData);
// Find the clip being moved
const sourceTrack = tracks.find(
@@ -1480,10 +1495,17 @@ function TimelineTrackContent({ return;
}
+ // Adjust position based on where user clicked on the clip
+ const adjustedStartTime = snappedTime - clickOffsetTime;
+ const finalStartTime = Math.max(
+ 0,
+ Math.round(adjustedStartTime * 10) / 10
+ );
+
// Check for overlaps with existing clips (excluding the moving clip itself)
const movingClipDuration =
movingClip.duration - movingClip.trimStart - movingClip.trimEnd;
- const movingClipEnd = snappedTime + movingClipDuration;
+ const movingClipEnd = finalStartTime + movingClipDuration;
const hasOverlap = track.clips.some((existingClip) => {
// Skip the clip being moved if it's on the same track
@@ -1498,7 +1520,7 @@ function TimelineTrackContent({ existingClip.trimEnd);
// Check if clips overlap
- return snappedTime < existingEnd && movingClipEnd > existingStart;
+ return finalStartTime < existingEnd && movingClipEnd > existingStart;
});
if (hasOverlap) {
@@ -1510,12 +1532,12 @@ function TimelineTrackContent({ if (fromTrackId === track.id) {
// Moving within same track
- updateClipStartTime(track.id, clipId, snappedTime);
+ updateClipStartTime(track.id, clipId, finalStartTime);
} else {
// Moving to different track
moveClipToTrack(fromTrackId, track.id, clipId);
requestAnimationFrame(() => {
- updateClipStartTime(track.id, clipId, snappedTime);
+ updateClipStartTime(track.id, clipId, finalStartTime);
});
}
} else if (hasMediaItem) {
@@ -1612,6 +1634,7 @@ function TimelineTrackContent({ src={mediaItem.url}
alt={mediaItem.name}
className="w-full h-full object-cover"
+ draggable={false}
/>
</div>
);
@@ -1625,6 +1648,7 @@ function TimelineTrackContent({ src={mediaItem.thumbnailUrl}
alt={mediaItem.name}
className="w-full h-full object-cover rounded-sm"
+ draggable={false}
/>
</div>
<span className="text-xs text-foreground/80 truncate flex-1">
@@ -1684,13 +1708,7 @@ function TimelineTrackContent({ return (
<div
- className={`w-full h-full transition-all duration-150 ease-out ${
- isDraggedOver
- ? wouldOverlap
- ? "bg-red-500/15 border-2 border-dashed border-red-400 shadow-lg"
- : "bg-blue-500/15 border-2 border-dashed border-blue-400 shadow-lg"
- : "hover:bg-muted/20"
- }`}
+ className="w-full h-full hover:bg-muted/20"
onContextMenu={(e) => {
e.preventDefault();
// Only show track menu if we didn't click on a clip
@@ -1744,7 +1762,7 @@ function TimelineTrackContent({ return (
<div
key={clip.id}
- className={`timeline-clip absolute h-full border transition-all duration-200 ${getTrackColor(track.type)} flex items-center py-3 min-w-[80px] overflow-hidden group hover:shadow-lg ${isSelected ? "ring-2 ring-blue-500 z-10" : ""}`}
+ className={`timeline-clip absolute h-full border ${getTrackColor(track.type)} flex items-center py-3 min-w-[80px] overflow-hidden group hover:shadow-lg ${isSelected ? "ring-2 ring-blue-500 z-10" : ""}`}
style={{ width: `${clipWidth}px`, left: `${clipLeft}px` }}
onClick={(e) => {
e.stopPropagation();
@@ -1835,30 +1853,6 @@ function TimelineTrackContent({ </div>
);
})}
-
- {/* Drop position indicator */}
- {isDraggedOver && dropPosition !== null && (
- <div
- className={`absolute top-0 bottom-0 w-1 pointer-events-none z-30 transition-all duration-75 ease-out ${wouldOverlap ? "bg-red-500" : "bg-blue-500"}`}
- style={{
- left: `${dropPosition * 50 * zoomLevel}px`,
- transform: "translateX(-50%)",
- }}
- >
- <div
- className={`absolute -top-2 left-1/2 transform -translate-x-1/2 w-3 h-3 rounded-full border-2 border-white shadow-md ${wouldOverlap ? "bg-red-500" : "bg-blue-500"}`}
- />
- <div
- className={`absolute -bottom-2 left-1/2 transform -translate-x-1/2 w-3 h-3 rounded-full border-2 border-white shadow-md ${wouldOverlap ? "bg-red-500" : "bg-blue-500"}`}
- />
- <div
- className={`absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-xs text-white px-1 py-0.5 rounded whitespace-nowrap ${wouldOverlap ? "bg-red-500" : "bg-blue-500"}`}
- >
- {wouldOverlap ? "⚠️" : ""}
- {dropPosition.toFixed(1)}s
- </div>
- </div>
- )}
</>
)}
</div>
|
