diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-04-18 01:40:18 +0200 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-04-18 01:40:18 +0200 |
| commit | 011f51051c9750706978d6c89ce84dd15560c335 (patch) | |
| tree | 2e7b23ebebb798934d6cb6aeace855f31699b633 /apps/web/src/lib/timeline/group-resize | |
| parent | c46d28891fee62ca2d3132da876af6411a41d9da (diff) | |
| download | opencut-011f51051c9750706978d6c89ce84dd15560c335.tar.gz | |
feat: clip volume line and timeline waveform improvements
Diffstat (limited to 'apps/web/src/lib/timeline/group-resize')
| -rw-r--r-- | apps/web/src/lib/timeline/group-resize/compute-resize.ts | 97 |
1 files changed, 28 insertions, 69 deletions
diff --git a/apps/web/src/lib/timeline/group-resize/compute-resize.ts b/apps/web/src/lib/timeline/group-resize/compute-resize.ts index 8d7d9b2d..5730b117 100644 --- a/apps/web/src/lib/timeline/group-resize/compute-resize.ts +++ b/apps/web/src/lib/timeline/group-resize/compute-resize.ts @@ -44,14 +44,34 @@ export function computeGroupResize({ ? minimumDeltaTime : Math.min(maximumDeltaTime, Math.max(minimumDeltaTime, deltaTime)); + // Snap the drag delta to a frame exactly once, then derive every patch + // field from that single snapped value. This keeps the invariant + // `trimStart + duration*rate + trimEnd == sourceDuration` exact: the same + // delta is added on one side of the element and removed from the other, + // so the rounding cancels by construction. Per-field rounding (the old + // approach) couldn't preserve this because the individual rounds don't + // compose when `sourceDuration` isn't frame-aligned. + const snappedDeltaTime = + roundToFrame({ time: clampedDeltaTime, rate: fps }) ?? clampedDeltaTime; + // Re-clamp after rounding. Bounds derived from other elements are + // frame-aligned, so this is normally a no-op; at the source-extent limit + // the bound may not be frame-aligned, and honouring the bound takes + // precedence over frame alignment (you can't extend past real content). + const finalDeltaTime = + minimumDeltaTime > maximumDeltaTime + ? minimumDeltaTime + : Math.min( + maximumDeltaTime, + Math.max(minimumDeltaTime, snappedDeltaTime), + ); + return { - deltaTime: Object.is(clampedDeltaTime, -0) ? 0 : clampedDeltaTime, + deltaTime: Object.is(finalDeltaTime, -0) ? 0 : finalDeltaTime, updates: members.map((member) => buildResizeUpdate({ member, side, - deltaTime: clampedDeltaTime, - fps, + deltaTime: finalDeltaTime, }), ), }; @@ -61,98 +81,37 @@ function buildResizeUpdate({ member, side, deltaTime, - fps, }: { member: GroupResizeMember; side: ResizeSide; deltaTime: number; - fps: ComputeGroupResizeArgs["fps"]; }): GroupResizeUpdate { - const totalSourceDuration = getSourceDuration({ member }); const sourceDelta = getSourceDeltaForClipDelta({ member, clipDelta: deltaTime, }); - const visibleSourceSpan = getVisibleSourceSpanForDuration({ - member, - duration: member.duration, - }); if (side === "left") { - if (deltaTime < 0 && member.sourceDuration == null) { - const rawStartTime = member.startTime + deltaTime; - const rawDuration = member.duration - deltaTime; - return { - trackId: member.trackId, - elementId: member.elementId, - patch: { - trimStart: Math.max(0, member.trimStart + sourceDelta), - trimEnd: member.trimEnd, - startTime: - roundToFrame({ time: rawStartTime, rate: fps }) ?? rawStartTime, - duration: - roundToFrame({ time: rawDuration, rate: fps }) ?? rawDuration, - }, - }; - } - - const nextTrimStart = Math.max(0, member.trimStart + sourceDelta); - const nextVisibleSourceSpan = Math.max( - 0, - totalSourceDuration - nextTrimStart - member.trimEnd, - ); - const rawDuration = getDurationForVisibleSourceSpan({ - member, - sourceSpan: nextVisibleSourceSpan, - }); - const nextDuration = - roundToFrame({ time: rawDuration, rate: fps }) ?? rawDuration; - const rawStartTime = member.startTime + (member.duration - nextDuration); return { trackId: member.trackId, elementId: member.elementId, patch: { - trimStart: - roundToFrame({ time: nextTrimStart, rate: fps }) ?? nextTrimStart, + trimStart: Math.max(0, member.trimStart + sourceDelta), trimEnd: member.trimEnd, - startTime: - roundToFrame({ time: rawStartTime, rate: fps }) ?? rawStartTime, - duration: nextDuration, + startTime: member.startTime + deltaTime, + duration: member.duration - deltaTime, }, }; } - const nextVisibleSourceSpan = Math.max(0, visibleSourceSpan + sourceDelta); - if (deltaTime > 0 && member.sourceDuration == null) { - const rawDuration = member.duration + deltaTime; - return { - trackId: member.trackId, - elementId: member.elementId, - patch: { - trimStart: member.trimStart, - trimEnd: Math.max(0, member.trimEnd - sourceDelta), - startTime: member.startTime, - duration: roundToFrame({ time: rawDuration, rate: fps }) ?? rawDuration, - }, - }; - } - - const nextTrimEnd = Math.max( - 0, - totalSourceDuration - member.trimStart - nextVisibleSourceSpan, - ); - const rawDuration = getDurationForVisibleSourceSpan({ - member, - sourceSpan: nextVisibleSourceSpan, - }); return { trackId: member.trackId, elementId: member.elementId, patch: { trimStart: member.trimStart, - trimEnd: roundToFrame({ time: nextTrimEnd, rate: fps }) ?? nextTrimEnd, + trimEnd: Math.max(0, member.trimEnd - sourceDelta), startTime: member.startTime, - duration: roundToFrame({ time: rawDuration, rate: fps }) ?? rawDuration, + duration: member.duration + deltaTime, }, }; } |
