summaryrefslogtreecommitdiff
path: root/apps/web/src/timeline/controllers/element-interaction-controller.ts
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/timeline/controllers/element-interaction-controller.ts')
-rw-r--r--apps/web/src/timeline/controllers/element-interaction-controller.ts126
1 files changed, 83 insertions, 43 deletions
diff --git a/apps/web/src/timeline/controllers/element-interaction-controller.ts b/apps/web/src/timeline/controllers/element-interaction-controller.ts
index cc19daaa..5afc3a93 100644
--- a/apps/web/src/timeline/controllers/element-interaction-controller.ts
+++ b/apps/web/src/timeline/controllers/element-interaction-controller.ts
@@ -7,9 +7,17 @@ import {
type MoveGroup,
} from "@/timeline/group-move";
import { BASE_TIMELINE_PIXELS_PER_SECOND } from "@/timeline/scale";
-import { TICKS_PER_SECOND } from "@/wasm";
+import {
+ maxMediaTime,
+ type MediaTime,
+ mediaTime,
+ roundFrameTime,
+ subMediaTime,
+ TICKS_PER_SECOND,
+ ZERO_MEDIA_TIME,
+} from "@/wasm";
import { TIMELINE_DRAG_THRESHOLD_PX } from "@/timeline/components/interaction";
-import { roundToFrame, type FrameRate } from "opencut-wasm";
+import type { FrameRate } from "opencut-wasm";
import { computeDropTarget } from "@/timeline/components/drop-target";
import { getMouseTimeFromClientX } from "@/timeline/drag-utils";
import { generateUUID } from "@/utils/id";
@@ -52,7 +60,7 @@ export interface ElementSelectionApi {
}
export interface PlaybackReader {
- getCurrentTime: () => number;
+ getCurrentTime: () => MediaTime;
}
export interface TimelineOps {
@@ -86,8 +94,8 @@ interface MousedownSnapshot {
readonly origin: Point;
readonly elementId: string;
readonly trackId: string;
- readonly startElementTime: number;
- readonly clickOffsetTime: number;
+ readonly startElementTime: MediaTime;
+ readonly clickOffsetTime: MediaTime;
readonly selectedElements: readonly ElementRef[];
}
@@ -100,7 +108,7 @@ interface DragProgress {
// indicator, drop-line, commit path) see the same entity every frame
// instead of a churning UUID.
reservedNewTrackIds: readonly string[];
- currentTime: number;
+ currentTime: MediaTime;
currentMouseX: number;
currentMouseY: number;
groupMoveResult: GroupMoveResult | null;
@@ -116,20 +124,27 @@ const IDLE_VIEW: ElementDragView = { kind: "idle" };
// --- Pure helpers ---
-function pixelToClickOffsetTime(
- clientX: number,
- elementRect: DOMRect,
- zoomLevel: number,
-): number {
+function pixelToClickOffsetTime({
+ clientX,
+ elementRect,
+ zoomLevel,
+}: {
+ clientX: number;
+ elementRect: DOMRect;
+ zoomLevel: number;
+}): MediaTime {
const clickOffsetX = clientX - elementRect.left;
const seconds = clickOffsetX / (BASE_TIMELINE_PIXELS_PER_SECOND * zoomLevel);
- return Math.round(seconds * TICKS_PER_SECOND);
+ return mediaTime({ ticks: Math.round(seconds * TICKS_PER_SECOND) });
}
-function verticalDirection(
- startMouseY: number,
- currentMouseY: number,
-): "up" | "down" | null {
+function verticalDirection({
+ startMouseY,
+ currentMouseY,
+}: {
+ startMouseY: number;
+ currentMouseY: number;
+}): "up" | "down" | null {
if (currentMouseY < startMouseY) return "up";
if (currentMouseY > startMouseY) return "down";
return null;
@@ -139,7 +154,13 @@ function orderedTracks(sceneTracks: SceneTracks): TimelineTrack[] {
return [...sceneTracks.overlay, sceneTracks.main, ...sceneTracks.audio];
}
-function movedPastDragThreshold(current: Point, origin: Point): boolean {
+function movedPastDragThreshold({
+ current,
+ origin,
+}: {
+ current: Point;
+ origin: Point;
+}): boolean {
return (
Math.abs(current.x - origin.x) > TIMELINE_DRAG_THRESHOLD_PX ||
Math.abs(current.y - origin.y) > TIMELINE_DRAG_THRESHOLD_PX
@@ -156,17 +177,20 @@ function frameSnappedMouseTime({
clientX: number;
scrollContainer: HTMLDivElement;
zoomLevel: number;
- clickOffsetTime: number;
+ clickOffsetTime: MediaTime;
fps: FrameRate;
-}): number {
+}): MediaTime {
const mouseTime = getMouseTimeFromClientX({
clientX,
containerRect: scrollContainer.getBoundingClientRect(),
zoomLevel,
scrollLeft: scrollContainer.scrollLeft,
});
- const adjusted = Math.max(0, mouseTime - clickOffsetTime);
- return roundToFrame({ time: adjusted, rate: fps }) ?? adjusted;
+ const adjusted = maxMediaTime({
+ a: ZERO_MEDIA_TIME,
+ b: subMediaTime({ a: mouseTime, b: clickOffsetTime }),
+ });
+ return roundFrameTime({ time: adjusted, fps });
}
function resolveDropTarget({
@@ -187,7 +211,7 @@ function resolveDropTarget({
tracks: SceneTracks;
viewport: ViewportAdapter;
zoomLevel: number;
- snappedTime: number;
+ snappedTime: MediaTime;
verticalDragDirection: "up" | "down" | null;
}): DropTarget | null {
const containerRect = viewport
@@ -231,7 +255,7 @@ function resolveGroupMoveForDrop({
}: {
group: MoveGroup;
tracks: SceneTracks;
- anchorStartTime: number;
+ anchorStartTime: MediaTime;
dropTarget: DropTarget;
reservedNewTrackIds: readonly string[];
}): GroupMoveResult | null {
@@ -286,7 +310,7 @@ export class ElementInteractionController {
get view(): ElementDragView {
if (this.session.kind !== "dragging") return IDLE_VIEW;
const { mousedown, drag } = this.session;
- const memberTimeOffsets = new Map<string, number>();
+ const memberTimeOffsets = new Map<string, MediaTime>();
for (const member of drag.moveGroup.members) {
memberTimeOffsets.set(member.elementId, member.timeOffset);
}
@@ -363,11 +387,11 @@ export class ElementInteractionController {
elementId: element.id,
trackId: track.id,
startElementTime: element.startTime,
- clickOffsetTime: pixelToClickOffsetTime(
- event.clientX,
- event.currentTarget.getBoundingClientRect(),
- this.deps.viewport.getZoomLevel(),
- ),
+ clickOffsetTime: pixelToClickOffsetTime({
+ clientX: event.clientX,
+ elementRect: event.currentTarget.getBoundingClientRect(),
+ zoomLevel: this.deps.viewport.getZoomLevel(),
+ }),
selectedElements,
},
};
@@ -426,10 +450,13 @@ export class ElementInteractionController {
this.notify();
}
- private snapResult(
- frameSnappedTime: number,
- group: MoveGroup,
- ): { snappedTime: number; snapPoint: SnapPoint | null } {
+ private snapResult({
+ frameSnappedTime,
+ group,
+ }: {
+ frameSnappedTime: MediaTime;
+ group: MoveGroup;
+ }): { snappedTime: MediaTime; snapPoint: SnapPoint | null } {
const { snap, input, scene, viewport, playback } = this.deps;
if (!snap.isEnabled() || input.isShiftHeld()) {
@@ -461,7 +488,7 @@ export class ElementInteractionController {
clientY: number;
mousedown: MousedownSnapshot;
drag: DragProgress;
- snappedTime: number;
+ snappedTime: MediaTime;
}): void {
const { scene, viewport } = this.deps;
const tracks = scene.getTracks();
@@ -476,7 +503,10 @@ export class ElementInteractionController {
viewport,
zoomLevel,
snappedTime,
- verticalDragDirection: verticalDirection(mousedown.origin.y, clientY),
+ verticalDragDirection: verticalDirection({
+ startMouseY: mousedown.origin.y,
+ currentMouseY: clientY,
+ }),
});
const nextGroupMoveResult = anchorDropTarget
@@ -532,7 +562,12 @@ export class ElementInteractionController {
clientY: number;
scrollContainer: HTMLDivElement;
}): void {
- if (!movedPastDragThreshold({ x: clientX, y: clientY }, mousedown.origin)) {
+ if (
+ !movedPastDragThreshold({
+ current: { x: clientX, y: clientY },
+ origin: mousedown.origin,
+ })
+ ) {
return;
}
@@ -557,10 +592,10 @@ export class ElementInteractionController {
clickOffsetTime: mousedown.clickOffsetTime,
fps,
});
- const { snappedTime, snapPoint } = this.snapResult(
+ const { snappedTime, snapPoint } = this.snapResult({
frameSnappedTime,
- moveGroup,
- );
+ group: moveGroup,
+ });
// Ensure the anchor is selected before we render the drag — covers the
// case where the selection store hasn't committed the mousedown-time
@@ -621,10 +656,10 @@ export class ElementInteractionController {
clickOffsetTime: mousedown.clickOffsetTime,
fps,
});
- const { snappedTime, snapPoint } = this.snapResult(
+ const { snappedTime, snapPoint } = this.snapResult({
frameSnappedTime,
- drag.moveGroup,
- );
+ group: drag.moveGroup,
+ });
drag.currentTime = snappedTime;
drag.currentMouseX = clientX;
@@ -655,7 +690,12 @@ export class ElementInteractionController {
// If the drag returned within the click threshold of its origin, treat
// this as a cancel rather than a commit — the user dragged then put the
// element back.
- if (!movedPastDragThreshold({ x: clientX, y: clientY }, mousedown.origin)) {
+ if (
+ !movedPastDragThreshold({
+ current: { x: clientX, y: clientY },
+ origin: mousedown.origin,
+ })
+ ) {
this.lastGestureWasDrag = false;
this.finishSession();
return;