summaryrefslogtreecommitdiff
path: root/apps/web/src/timeline/group-resize
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-04-26 17:31:28 +0200
committerMaze Winther <mazewinther@gmail.com>2026-04-26 17:31:28 +0200
commit56ca09692a308b22455420d11376ff8d41244c47 (patch)
tree4eca5ae032451c5db954801008aa9f164b2e2b1d /apps/web/src/timeline/group-resize
parente6c6193638e4247ff1e6f2df40de2a057112c773 (diff)
downloadopencut-56ca09692a308b22455420d11376ff8d41244c47.tar.gz
refactor: thinner hooks
Diffstat (limited to 'apps/web/src/timeline/group-resize')
-rw-r--r--apps/web/src/timeline/group-resize/compute-resize.ts218
-rw-r--r--apps/web/src/timeline/group-resize/types.ts27
2 files changed, 156 insertions, 89 deletions
diff --git a/apps/web/src/timeline/group-resize/compute-resize.ts b/apps/web/src/timeline/group-resize/compute-resize.ts
index bbe829cc..465531bf 100644
--- a/apps/web/src/timeline/group-resize/compute-resize.ts
+++ b/apps/web/src/timeline/group-resize/compute-resize.ts
@@ -1,9 +1,20 @@
-import { roundToFrame } from "opencut-wasm";
import {
getSourceSpanAtClipTime,
getTimelineDurationForSourceSpan,
} from "@/retime";
-import { TICKS_PER_SECOND, roundMediaTime } from "@/wasm";
+import {
+ addMediaTime,
+ clampMediaTime,
+ maxMediaTime,
+ type MediaTime,
+ mediaTime,
+ minMediaTime,
+ roundFrameTicks,
+ roundMediaTime,
+ subMediaTime,
+ TICKS_PER_SECOND,
+ ZERO_MEDIA_TIME,
+} from "@/wasm";
import type {
ComputeGroupResizeArgs,
GroupResizeMember,
@@ -18,31 +29,54 @@ export function computeGroupResize({
deltaTime,
fps,
}: ComputeGroupResizeArgs): GroupResizeResult {
- const minDuration = Math.round(
- (TICKS_PER_SECOND * fps.denominator) / fps.numerator,
- );
- const minimumDeltaTime = Math.max(
- ...members.map((member) =>
- getMinimumAllowedDeltaTime({
- member,
- side,
- minDuration,
- }),
- ),
- );
- const maximumDeltaTime = Math.min(
- ...members.map((member) =>
- getMaximumAllowedDeltaTime({
+ if (members.length === 0) {
+ return { deltaTime: ZERO_MEDIA_TIME, updates: [] };
+ }
+
+ const minDuration = mediaTime({
+ ticks: Math.round((TICKS_PER_SECOND * fps.denominator) / fps.numerator),
+ });
+ let minimumDeltaTime = getMinimumAllowedDeltaTime({
+ member: members[0],
+ side,
+ minDuration,
+ });
+ let maximumDeltaTime = getMaximumAllowedDeltaTime({
+ member: members[0],
+ side,
+ minDuration,
+ });
+
+ for (const member of members.slice(1)) {
+ minimumDeltaTime = maxMediaTime({
+ a: minimumDeltaTime,
+ b: getMinimumAllowedDeltaTime({
member,
side,
minDuration,
}),
- ),
- );
+ });
+ const memberMaximum = getMaximumAllowedDeltaTime({
+ member,
+ side,
+ minDuration,
+ });
+ if (memberMaximum !== null) {
+ maximumDeltaTime =
+ maximumDeltaTime === null
+ ? memberMaximum
+ : minMediaTime({ a: maximumDeltaTime, b: memberMaximum });
+ }
+ }
+
const clampedDeltaTime =
- minimumDeltaTime > maximumDeltaTime
- ? minimumDeltaTime
- : Math.min(maximumDeltaTime, Math.max(minimumDeltaTime, deltaTime));
+ maximumDeltaTime === null
+ ? maxMediaTime({ a: minimumDeltaTime, b: deltaTime })
+ : clampMediaTime({
+ time: deltaTime,
+ min: minimumDeltaTime,
+ max: maximumDeltaTime,
+ });
// Snap the drag delta to a frame exactly once, then derive every patch
// field from that single snapped value. This keeps the invariant
@@ -51,22 +85,24 @@ export function computeGroupResize({
// 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;
+ const snappedDeltaTime = mediaTime({
+ ticks: roundFrameTicks({ ticks: clampedDeltaTime, fps }),
+ });
// 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),
- );
+ maximumDeltaTime === null
+ ? maxMediaTime({ a: minimumDeltaTime, b: snappedDeltaTime })
+ : clampMediaTime({
+ time: snappedDeltaTime,
+ min: minimumDeltaTime,
+ max: maximumDeltaTime,
+ });
return {
- deltaTime: Object.is(finalDeltaTime, -0) ? 0 : finalDeltaTime,
+ deltaTime: Object.is(finalDeltaTime, -0) ? ZERO_MEDIA_TIME : finalDeltaTime,
updates: members.map((member) =>
buildResizeUpdate({
member,
@@ -84,7 +120,7 @@ function buildResizeUpdate({
}: {
member: GroupResizeMember;
side: ResizeSide;
- deltaTime: number;
+ deltaTime: MediaTime;
}): GroupResizeUpdate {
const sourceDelta = getSourceDeltaForClipDelta({
member,
@@ -96,10 +132,13 @@ function buildResizeUpdate({
trackId: member.trackId,
elementId: member.elementId,
patch: {
- trimStart: Math.max(0, member.trimStart + sourceDelta),
+ trimStart: maxMediaTime({
+ a: ZERO_MEDIA_TIME,
+ b: addMediaTime({ a: member.trimStart, b: sourceDelta }),
+ }),
trimEnd: member.trimEnd,
- startTime: member.startTime + deltaTime,
- duration: member.duration - deltaTime,
+ startTime: addMediaTime({ a: member.startTime, b: deltaTime }),
+ duration: subMediaTime({ a: member.duration, b: deltaTime }),
},
};
}
@@ -109,9 +148,12 @@ function buildResizeUpdate({
elementId: member.elementId,
patch: {
trimStart: member.trimStart,
- trimEnd: Math.max(0, member.trimEnd - sourceDelta),
+ trimEnd: maxMediaTime({
+ a: ZERO_MEDIA_TIME,
+ b: subMediaTime({ a: member.trimEnd, b: sourceDelta }),
+ }),
startTime: member.startTime,
- duration: member.duration + deltaTime,
+ duration: addMediaTime({ a: member.duration, b: deltaTime }),
},
};
}
@@ -123,29 +165,37 @@ function getMinimumAllowedDeltaTime({
}: {
member: GroupResizeMember;
side: ResizeSide;
- minDuration: number;
-}): number {
+ minDuration: MediaTime;
+}): MediaTime {
if (side === "right") {
- return minDuration - member.duration;
+ return subMediaTime({ a: minDuration, b: member.duration });
}
- const leftNeighborFloor = Number.isFinite(member.leftNeighborBound)
- ? member.leftNeighborBound - member.startTime
- : -member.startTime;
+ const leftNeighborFloor =
+ member.leftNeighborBound !== null
+ ? subMediaTime({ a: member.leftNeighborBound, b: member.startTime })
+ : subMediaTime({ a: ZERO_MEDIA_TIME, b: member.startTime });
if (member.sourceDuration == null) {
return leftNeighborFloor;
}
- const maximumSourceExtension =
- getDurationForVisibleSourceSpan({
+ const maximumSourceExtension = subMediaTime({
+ a: getDurationForVisibleSourceSpan({
member,
- sourceSpan:
- getVisibleSourceSpanForDuration({
+ sourceSpan: addMediaTime({
+ a: getVisibleSourceSpanForDuration({
member,
duration: member.duration,
- }) + member.trimStart,
- }) - member.duration;
- return Math.max(leftNeighborFloor, -maximumSourceExtension);
+ }),
+ b: member.trimStart,
+ }),
+ }),
+ b: member.duration,
+ });
+ return maxMediaTime({
+ a: leftNeighborFloor,
+ b: subMediaTime({ a: ZERO_MEDIA_TIME, b: maximumSourceExtension }),
+ });
}
function getMaximumAllowedDeltaTime({
@@ -155,26 +205,38 @@ function getMaximumAllowedDeltaTime({
}: {
member: GroupResizeMember;
side: ResizeSide;
- minDuration: number;
-}): number {
+ minDuration: MediaTime;
+}): MediaTime | null {
if (side === "left") {
- return member.duration - minDuration;
+ return subMediaTime({ a: member.duration, b: minDuration });
}
- const rightNeighborCeiling = Number.isFinite(member.rightNeighborBound)
- ? member.rightNeighborBound - (member.startTime + member.duration)
- : Infinity;
+ const rightNeighborCeiling =
+ member.rightNeighborBound === null
+ ? null
+ : subMediaTime({
+ a: member.rightNeighborBound,
+ b: addMediaTime({ a: member.startTime, b: member.duration }),
+ });
if (member.sourceDuration == null) {
return rightNeighborCeiling;
}
- const maximumVisibleSourceSpan =
- getSourceDuration({ member }) - member.trimStart;
+ const maximumVisibleSourceSpan = subMediaTime({
+ a: getSourceDuration({ member }),
+ b: member.trimStart,
+ });
const maximumDuration = getDurationForVisibleSourceSpan({
member,
sourceSpan: maximumVisibleSourceSpan,
});
- return Math.min(rightNeighborCeiling, maximumDuration - member.duration);
+ const sourceDurationCeiling = subMediaTime({
+ a: maximumDuration,
+ b: member.duration,
+ });
+ return rightNeighborCeiling === null
+ ? sourceDurationCeiling
+ : minMediaTime({ a: rightNeighborCeiling, b: sourceDurationCeiling });
}
function getSourceDeltaForClipDelta({
@@ -182,8 +244,8 @@ function getSourceDeltaForClipDelta({
clipDelta,
}: {
member: GroupResizeMember;
- clipDelta: number;
-}): number {
+ clipDelta: MediaTime;
+}): MediaTime {
if (!member.retime) {
return clipDelta;
}
@@ -206,15 +268,17 @@ function getVisibleSourceSpanForDuration({
duration,
}: {
member: GroupResizeMember;
- duration: number;
-}): number {
+ duration: MediaTime;
+}): MediaTime {
if (!member.retime) {
return duration;
}
- return getSourceSpanAtClipTime({
- clipTime: duration,
- retime: member.retime,
+ return roundMediaTime({
+ time: getSourceSpanAtClipTime({
+ clipTime: duration,
+ retime: member.retime,
+ }),
});
}
@@ -223,8 +287,8 @@ function getDurationForVisibleSourceSpan({
sourceSpan,
}: {
member: GroupResizeMember;
- sourceSpan: number;
-}): number {
+ sourceSpan: MediaTime;
+}): MediaTime {
if (!member.retime) {
return sourceSpan;
}
@@ -237,17 +301,19 @@ function getDurationForVisibleSourceSpan({
});
}
-function getSourceDuration({ member }: { member: GroupResizeMember }): number {
- if (typeof member.sourceDuration === "number") {
+function getSourceDuration({ member }: { member: GroupResizeMember }): MediaTime {
+ if (member.sourceDuration != null) {
return member.sourceDuration;
}
- return (
- member.trimStart +
- getVisibleSourceSpanForDuration({
+ return addMediaTime({
+ a: addMediaTime({
+ a: member.trimStart,
+ b: getVisibleSourceSpanForDuration({
member,
duration: member.duration,
- }) +
- member.trimEnd
- );
+ }),
+ }),
+ b: member.trimEnd,
+ });
}
diff --git a/apps/web/src/timeline/group-resize/types.ts b/apps/web/src/timeline/group-resize/types.ts
index b07e6fe6..5c0f77f6 100644
--- a/apps/web/src/timeline/group-resize/types.ts
+++ b/apps/web/src/timeline/group-resize/types.ts
@@ -1,36 +1,37 @@
import type { FrameRate } from "opencut-wasm";
import type { ElementRef, RetimeConfig } from "@/timeline/types";
+import type { MediaTime } from "@/wasm";
export type ResizeSide = "left" | "right";
export interface GroupResizeMember extends ElementRef {
- startTime: number;
- duration: number;
- trimStart: number;
- trimEnd: number;
- sourceDuration?: number;
+ startTime: MediaTime;
+ duration: MediaTime;
+ trimStart: MediaTime;
+ trimEnd: MediaTime;
+ sourceDuration?: MediaTime;
retime?: RetimeConfig;
- leftNeighborBound: number;
- rightNeighborBound: number;
+ leftNeighborBound: MediaTime | null;
+ rightNeighborBound: MediaTime | null;
}
export interface GroupResizeUpdate extends ElementRef {
patch: {
- trimStart: number;
- trimEnd: number;
- startTime: number;
- duration: number;
+ trimStart: MediaTime;
+ trimEnd: MediaTime;
+ startTime: MediaTime;
+ duration: MediaTime;
};
}
export interface GroupResizeResult {
- deltaTime: number;
+ deltaTime: MediaTime;
updates: GroupResizeUpdate[];
}
export interface ComputeGroupResizeArgs {
members: GroupResizeMember[];
side: ResizeSide;
- deltaTime: number;
+ deltaTime: MediaTime;
fps: FrameRate;
}