diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-03-02 13:13:07 +0100 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-03-02 13:13:07 +0100 |
| commit | e7dcb586c0408255a0bf788e355fdb6d50d9b923 (patch) | |
| tree | 74ff0fc0f1ba501a075342db9b4e454450926154 /apps/web/src/hooks/timeline/element/use-element-resize.ts | |
| parent | 93bea01c9ed3fcbebc64c9cef382a51d24c925ed (diff) | |
| download | opencut-e7dcb586c0408255a0bf788e355fdb6d50d9b923.tar.gz | |
feat: Clip effects, asset sorting, and timeline improvements
Major features and improvements:
* **Clip Effects**:
* Added UI in Properties Panel to manage effects on video/image clips (add, remove, toggle, reorder).
* Implemented dynamic parameter fields for effects.
* Added support for keyframing effect parameters.
* **Assets Panel**:
* Added sorting options: Name, Type, Duration, and File Size.
* Persisted view preferences (grid/list mode, sort order) to local storage.
* Refactored media item rendering and drag interactions.
* **Timeline & Interaction**:
* **Keyframe Dragging**: Added ability to drag keyframes directly on the timeline element.
* **Resizing**: Improved resize logic to respect neighboring clips (prevents overlaps).
* **Visuals**: Implemented tiled background rendering for video/image clips on the timeline.
* **Shortcuts**: Added "Deselect All" action bound to the `Escape` key.
* **Fixes**: Corrected drag-and-drop coordinate calculations when the timeline track area is scrolled.
* **Text Elements**:
* Refactored text background storage to use an explicit `enabled` flag.
* Added `V8toV9` storage migration to update existing projects.
* **Architecture**:
* Moved export state management to `ProjectManager` for better lifecycle handling.
* Refactored `PropertiesPanel` sections to be more composable (custom headers, borders).
Diffstat (limited to 'apps/web/src/hooks/timeline/element/use-element-resize.ts')
| -rw-r--r-- | apps/web/src/hooks/timeline/element/use-element-resize.ts | 98 |
1 files changed, 73 insertions, 25 deletions
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 2340c9b2..0ae17cb6 100644 --- a/apps/web/src/hooks/timeline/element/use-element-resize.ts +++ b/apps/web/src/hooks/timeline/element/use-element-resize.ts @@ -137,18 +137,52 @@ export function useTimelineElementResize({ } onSnapPointChange?.(resizeSnapPoint); + const otherElements = track.elements.filter(({ id }) => id !== element.id); + const initialEndTime = resizing.initialStartTime + resizing.initialDuration; + + const rightNeighborBound = + resizing.side === "right" + ? otherElements + .filter(({ startTime }) => startTime >= initialEndTime) + .reduce((min, { startTime }) => Math.min(min, startTime), Infinity) + : Infinity; + + const leftNeighborBound = + resizing.side === "left" + ? otherElements + .filter( + ({ startTime, duration }) => + startTime + duration <= resizing.initialStartTime, + ) + .reduce( + (max, { startTime, duration }) => + Math.max(max, startTime + duration), + -Infinity, + ) + : -Infinity; + if (resizing.side === "left") { const sourceDuration = resizing.initialTrimStart + resizing.initialDuration + resizing.initialTrimEnd; + const minTrimStartForNeighbor = Number.isFinite(leftNeighborBound) + ? Math.max( + 0, + resizing.initialTrimStart + + (leftNeighborBound - resizing.initialStartTime), + ) + : 0; const maxAllowed = sourceDuration - resizing.initialTrimEnd - minDurationSeconds; const calculated = resizing.initialTrimStart + deltaTime; if (calculated >= 0 && calculated <= maxAllowed) { const newTrimStart = snapTimeToFrame({ - time: Math.min(maxAllowed, calculated), + time: Math.min( + maxAllowed, + Math.max(minTrimStartForNeighbor, calculated), + ), fps: projectFps, }); const trimDelta = newTrimStart - resizing.initialTrimStart; @@ -171,7 +205,16 @@ export function useTimelineElementResize({ if (canExtendElementDuration()) { const extensionAmount = Math.abs(calculated); const maxExtension = resizing.initialStartTime; - const actualExtension = Math.min(extensionAmount, maxExtension); + const actualExtension = Math.max( + 0, + Number.isFinite(leftNeighborBound) + ? Math.min( + extensionAmount, + maxExtension, + resizing.initialStartTime - leftNeighborBound, + ) + : Math.min(extensionAmount, maxExtension), + ); const newStartTime = snapTimeToFrame({ time: resizing.initialStartTime - actualExtension, fps: projectFps, @@ -188,7 +231,8 @@ export function useTimelineElementResize({ currentStartTimeRef.current = newStartTime; currentDurationRef.current = newDuration; } else { - const trimDelta = 0 - resizing.initialTrimStart; + const trimDelta = + minTrimStartForNeighbor - resizing.initialTrimStart; const newStartTime = snapTimeToFrame({ time: resizing.initialStartTime + trimDelta, fps: projectFps, @@ -198,10 +242,10 @@ export function useTimelineElementResize({ fps: projectFps, }); - setCurrentTrimStart(0); + setCurrentTrimStart(minTrimStartForNeighbor); setCurrentStartTime(newStartTime); setCurrentDuration(newDuration); - currentTrimStartRef.current = 0; + currentTrimStartRef.current = minTrimStartForNeighbor; currentStartTimeRef.current = newStartTime; currentDurationRef.current = newDuration; } @@ -212,6 +256,9 @@ export function useTimelineElementResize({ resizing.initialDuration + resizing.initialTrimEnd; const newTrimEnd = resizing.initialTrimEnd - deltaTime; + const maxAllowedDuration = Number.isFinite(rightNeighborBound) + ? rightNeighborBound - resizing.initialStartTime + : Infinity; if (newTrimEnd < 0) { if (canExtendElementDuration()) { @@ -219,7 +266,7 @@ export function useTimelineElementResize({ const baseDuration = resizing.initialDuration + resizing.initialTrimEnd; const newDuration = snapTimeToFrame({ - time: baseDuration + extensionNeeded, + time: Math.min(baseDuration + extensionNeeded, maxAllowedDuration), fps: projectFps, }); @@ -230,7 +277,10 @@ export function useTimelineElementResize({ } else { const extensionToLimit = resizing.initialTrimEnd; const newDuration = snapTimeToFrame({ - time: resizing.initialDuration + extensionToLimit, + time: Math.min( + resizing.initialDuration + extensionToLimit, + maxAllowedDuration, + ), fps: projectFps, }); @@ -240,9 +290,20 @@ export function useTimelineElementResize({ currentTrimEndRef.current = 0; } } else { + const minTrimEndForNeighbor = Number.isFinite(maxAllowedDuration) + ? Math.max( + 0, + resizing.initialDuration + + resizing.initialTrimEnd - + maxAllowedDuration, + ) + : 0; const maxTrimEnd = sourceDuration - resizing.initialTrimStart - minDurationSeconds; - const clampedTrimEnd = Math.min(maxTrimEnd, Math.max(0, newTrimEnd)); + const clampedTrimEnd = Math.min( + maxTrimEnd, + Math.max(minTrimEndForNeighbor, newTrimEnd), + ); const finalTrimEnd = snapTimeToFrame({ time: clampedTrimEnd, fps: projectFps, @@ -267,6 +328,7 @@ export function useTimelineElementResize({ snappingEnabled, editor, element.id, + track.elements, onSnapPointChange, canExtendElementDuration, isShiftHeldRef, @@ -285,26 +347,13 @@ export function useTimelineElementResize({ const startTimeChanged = finalStartTime !== resizing.initialStartTime; const durationChanged = finalDuration !== resizing.initialDuration; - if (trimStartChanged || trimEndChanged) { + if (trimStartChanged || trimEndChanged || startTimeChanged || durationChanged) { editor.timeline.updateElementTrim({ elementId: element.id, trimStart: finalTrimStart, trimEnd: finalTrimEnd, - }); - } - - if (startTimeChanged) { - editor.timeline.updateElementStartTime({ - elements: [{ trackId: track.id, elementId: element.id }], - startTime: finalStartTime, - }); - } - - if (durationChanged) { - editor.timeline.updateElementDuration({ - trackId: track.id, - elementId: element.id, - duration: finalDuration, + startTime: startTimeChanged ? finalStartTime : undefined, + duration: durationChanged ? finalDuration : undefined, }); } @@ -315,7 +364,6 @@ export function useTimelineElementResize({ resizing, editor.timeline, element.id, - track.id, onResizeStateChange, onSnapPointChange, ]); |
