mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
refactor: split effects and masks into dedicated rust crates, introduce MediaTime and FrameRate
This commit is contained in:
@@ -10,8 +10,9 @@ import { useEditor } from "@/hooks/use-editor";
|
||||
import { useShiftKey } from "@/hooks/use-shift-key";
|
||||
import { useElementSelection } from "@/hooks/timeline/element/use-element-selection";
|
||||
import { BASE_TIMELINE_PIXELS_PER_SECOND } from "@/lib/timeline/scale";
|
||||
import { TICKS_PER_SECOND } from "@/lib/wasm";
|
||||
import { TIMELINE_DRAG_THRESHOLD_PX } from "@/components/editor/panels/timeline/interaction";
|
||||
import { snapTimeToFrame } from "opencut-wasm";
|
||||
import { roundToFrame } from "opencut-wasm";
|
||||
import { computeDropTarget } from "@/components/editor/panels/timeline/drop-target";
|
||||
import { getMouseTimeFromClientX } from "@/lib/timeline/drag-utils";
|
||||
import { generateUUID } from "@/utils/id";
|
||||
@@ -67,7 +68,8 @@ function getClickOffsetTime({
|
||||
zoomLevel: number;
|
||||
}): number {
|
||||
const clickOffsetX = clientX - elementRect.left;
|
||||
return clickOffsetX / (BASE_TIMELINE_PIXELS_PER_SECOND * zoomLevel);
|
||||
const seconds = clickOffsetX / (BASE_TIMELINE_PIXELS_PER_SECOND * zoomLevel);
|
||||
return Math.round(seconds * TICKS_PER_SECOND);
|
||||
}
|
||||
|
||||
function getVerticalDragDirection({
|
||||
@@ -297,15 +299,17 @@ export function useElementInteraction({
|
||||
zoomLevel,
|
||||
scrollLeft,
|
||||
});
|
||||
const adjustedTime =
|
||||
mouseTime - pendingDragRef.current.clickOffsetTime;
|
||||
const snappedTime = snapTimeToFrame({
|
||||
time: adjustedTime,
|
||||
fps: activeProject.settings.fps,
|
||||
});
|
||||
startDrag({
|
||||
...pendingDragRef.current,
|
||||
initialCurrentTime: snappedTime,
|
||||
const adjustedTime = Math.max(
|
||||
0,
|
||||
mouseTime - pendingDragRef.current.clickOffsetTime,
|
||||
);
|
||||
const snappedTime = roundToFrame({
|
||||
time: adjustedTime,
|
||||
rate: activeProject.settings.fps,
|
||||
}) ?? adjustedTime;
|
||||
startDrag({
|
||||
...pendingDragRef.current,
|
||||
initialCurrentTime: snappedTime,
|
||||
initialCurrentMouseY: clientY,
|
||||
});
|
||||
startedDragThisEvent = true;
|
||||
@@ -343,9 +347,9 @@ export function useElementInteraction({
|
||||
zoomLevel,
|
||||
scrollLeft,
|
||||
});
|
||||
const adjustedTime = mouseTime - dragState.clickOffsetTime;
|
||||
const adjustedTime = Math.max(0, mouseTime - dragState.clickOffsetTime);
|
||||
const fps = activeProject.settings.fps;
|
||||
const frameSnappedTime = snapTimeToFrame({ time: adjustedTime, fps });
|
||||
const frameSnappedTime = roundToFrame({ time: adjustedTime, rate: fps }) ?? adjustedTime;
|
||||
|
||||
const sourceTrack = tracks.find(({ id }) => id === dragState.trackId);
|
||||
const movingElement = sourceTrack?.elements.find(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useEffect, useRef, useCallback } from "react";
|
||||
import { BASE_TIMELINE_PIXELS_PER_SECOND } from "@/lib/timeline/scale";
|
||||
import { snapTimeToFrame } from "opencut-wasm";
|
||||
import { TICKS_PER_SECOND } from "@/lib/wasm";
|
||||
import { roundToFrame } from "opencut-wasm";
|
||||
import type { TimelineElement, TimelineTrack } from "@/lib/timeline";
|
||||
import { useEditor } from "@/hooks/use-editor";
|
||||
import { useShiftKey } from "@/hooks/use-shift-key";
|
||||
@@ -185,13 +186,14 @@ export function useTimelineElementResize({
|
||||
({ clientX }: { clientX: number }) => {
|
||||
if (!resizing) return;
|
||||
|
||||
const deltaX = clientX - resizing.startX;
|
||||
let deltaTime =
|
||||
deltaX / (BASE_TIMELINE_PIXELS_PER_SECOND * zoomLevel);
|
||||
let resizeSnapPoint: SnapPoint | null = null;
|
||||
const deltaX = clientX - resizing.startX;
|
||||
let deltaTime = Math.round(
|
||||
(deltaX / (BASE_TIMELINE_PIXELS_PER_SECOND * zoomLevel)) * TICKS_PER_SECOND,
|
||||
);
|
||||
let resizeSnapPoint: SnapPoint | null = null;
|
||||
|
||||
const projectFps = editor.project.getActive().settings.fps;
|
||||
const minDurationSeconds = 1 / projectFps;
|
||||
const projectFps = editor.project.getActive().settings.fps;
|
||||
const minDuration = Math.round(TICKS_PER_SECOND * projectFps.denominator / projectFps.numerator);
|
||||
const shouldSnap = snappingEnabled && !isShiftHeldRef.current;
|
||||
if (shouldSnap) {
|
||||
const tracks = editor.timeline.getTracks();
|
||||
@@ -277,19 +279,19 @@ export function useTimelineElementResize({
|
||||
const maxAllowed =
|
||||
sourceDuration -
|
||||
resizing.initialTrimEnd -
|
||||
getVisibleSourceSpanForDuration(minDurationSeconds);
|
||||
getVisibleSourceSpanForDuration(minDuration);
|
||||
const calculated =
|
||||
resizing.initialTrimStart + getSourceDeltaForClipDelta(deltaTime);
|
||||
|
||||
if (calculated >= 0 && calculated <= maxAllowed) {
|
||||
const newTrimStart = snapTimeToFrame({ time: Math.min(maxAllowed, Math.max(minTrimStartForNeighbor, calculated)), fps: projectFps });
|
||||
const newTrimStart = roundToFrame({ time: Math.min(maxAllowed, Math.max(minTrimStartForNeighbor, calculated)), rate: projectFps }) ?? Math.min(maxAllowed, Math.max(minTrimStartForNeighbor, calculated));
|
||||
const visibleSourceSpan = Math.max(
|
||||
0,
|
||||
sourceDuration - newTrimStart - resizing.initialTrimEnd,
|
||||
);
|
||||
const newDuration = snapTimeToFrame({ time: getDurationForVisibleSourceSpan(visibleSourceSpan), fps: projectFps });
|
||||
const newDuration = roundToFrame({ time: getDurationForVisibleSourceSpan(visibleSourceSpan), rate: projectFps }) ?? getDurationForVisibleSourceSpan(visibleSourceSpan);
|
||||
const trimDelta = resizing.initialDuration - newDuration;
|
||||
const newStartTime = snapTimeToFrame({ time: resizing.initialStartTime + trimDelta, fps: projectFps });
|
||||
const newStartTime = roundToFrame({ time: resizing.initialStartTime + trimDelta, rate: projectFps }) ?? resizing.initialStartTime + trimDelta;
|
||||
|
||||
setCurrentTrimStart(newTrimStart);
|
||||
setCurrentStartTime(newStartTime);
|
||||
@@ -311,8 +313,8 @@ export function useTimelineElementResize({
|
||||
)
|
||||
: Math.min(extensionAmount, maxExtension),
|
||||
);
|
||||
const newStartTime = snapTimeToFrame({ time: resizing.initialStartTime - actualExtension, fps: projectFps });
|
||||
const newDuration = snapTimeToFrame({ time: resizing.initialDuration + actualExtension, fps: projectFps });
|
||||
const newStartTime = roundToFrame({ time: resizing.initialStartTime - actualExtension, rate: projectFps }) ?? resizing.initialStartTime - actualExtension;
|
||||
const newDuration = roundToFrame({ time: resizing.initialDuration + actualExtension, rate: projectFps }) ?? resizing.initialDuration + actualExtension;
|
||||
|
||||
setCurrentTrimStart(0);
|
||||
setCurrentStartTime(newStartTime);
|
||||
@@ -338,8 +340,8 @@ export function useTimelineElementResize({
|
||||
0,
|
||||
sourceDuration - newTrimStart - resizing.initialTrimEnd,
|
||||
);
|
||||
const newDuration = snapTimeToFrame({ time: getDurationForVisibleSourceSpan(visibleSourceSpan), fps: projectFps });
|
||||
const newStartTime = snapTimeToFrame({ time: resizing.initialStartTime + (resizing.initialDuration - newDuration), fps: projectFps });
|
||||
const newDuration = roundToFrame({ time: getDurationForVisibleSourceSpan(visibleSourceSpan), rate: projectFps }) ?? getDurationForVisibleSourceSpan(visibleSourceSpan);
|
||||
const newStartTime = roundToFrame({ time: resizing.initialStartTime + (resizing.initialDuration - newDuration), rate: projectFps }) ?? resizing.initialStartTime + (resizing.initialDuration - newDuration);
|
||||
|
||||
setCurrentTrimStart(newTrimStart);
|
||||
setCurrentStartTime(newStartTime);
|
||||
@@ -366,7 +368,7 @@ export function useTimelineElementResize({
|
||||
const extensionNeeded = Math.abs(newTrimEnd);
|
||||
const baseDuration =
|
||||
resizing.initialDuration + resizing.initialTrimEnd;
|
||||
const newDuration = snapTimeToFrame({ time: Math.min(baseDuration + extensionNeeded, maxAllowedDuration), fps: projectFps });
|
||||
const newDuration = roundToFrame({ time: Math.min(baseDuration + extensionNeeded, maxAllowedDuration), rate: projectFps }) ?? Math.min(baseDuration + extensionNeeded, maxAllowedDuration);
|
||||
|
||||
setCurrentDuration(newDuration);
|
||||
setCurrentTrimEnd(0);
|
||||
@@ -376,7 +378,7 @@ export function useTimelineElementResize({
|
||||
const unclampedDuration = getDurationForVisibleSourceSpan(
|
||||
Math.max(0, sourceDuration - resizing.initialTrimStart),
|
||||
);
|
||||
const newDuration = snapTimeToFrame({ time: Math.min(unclampedDuration, maxAllowedDuration), fps: projectFps });
|
||||
const newDuration = roundToFrame({ time: Math.min(unclampedDuration, maxAllowedDuration), rate: projectFps }) ?? Math.min(unclampedDuration, maxAllowedDuration);
|
||||
|
||||
setCurrentDuration(newDuration);
|
||||
setCurrentTrimEnd(0);
|
||||
@@ -395,17 +397,17 @@ export function useTimelineElementResize({
|
||||
const maxTrimEnd =
|
||||
sourceDuration -
|
||||
resizing.initialTrimStart -
|
||||
getVisibleSourceSpanForDuration(minDurationSeconds);
|
||||
getVisibleSourceSpanForDuration(minDuration);
|
||||
const clampedTrimEnd = Math.min(
|
||||
maxTrimEnd,
|
||||
Math.max(minTrimEndForNeighbor, newTrimEnd),
|
||||
);
|
||||
const finalTrimEnd = snapTimeToFrame({ time: clampedTrimEnd, fps: projectFps });
|
||||
const finalTrimEnd = roundToFrame({ time: clampedTrimEnd, rate: projectFps }) ?? clampedTrimEnd;
|
||||
const visibleSourceSpan = Math.max(
|
||||
0,
|
||||
sourceDuration - resizing.initialTrimStart - finalTrimEnd,
|
||||
);
|
||||
const newDuration = snapTimeToFrame({ time: getDurationForVisibleSourceSpan(visibleSourceSpan), fps: projectFps });
|
||||
const newDuration = roundToFrame({ time: getDurationForVisibleSourceSpan(visibleSourceSpan), rate: projectFps }) ?? getDurationForVisibleSourceSpan(visibleSourceSpan);
|
||||
|
||||
setCurrentTrimEnd(finalTrimEnd);
|
||||
setCurrentDuration(newDuration);
|
||||
|
||||
@@ -8,9 +8,10 @@ import {
|
||||
import { useEditor } from "@/hooks/use-editor";
|
||||
import { getKeyframeById } from "@/lib/animation";
|
||||
import { useKeyframeSelection } from "./use-keyframe-selection";
|
||||
import { snapTimeToFrame, getSnappedSeekTime } from "opencut-wasm";
|
||||
import { roundToFrame, snappedSeekTime } from "opencut-wasm";
|
||||
import { timelineTimeToSnappedPixels } from "@/lib/timeline";
|
||||
import { BASE_TIMELINE_PIXELS_PER_SECOND } from "@/lib/timeline/scale";
|
||||
import { TICKS_PER_SECOND } from "@/lib/wasm";
|
||||
import { TIMELINE_DRAG_THRESHOLD_PX } from "@/components/editor/panels/timeline/interaction";
|
||||
import { RetimeKeyframeCommand } from "@/lib/commands/timeline/element/keyframes/retime-keyframe";
|
||||
import { BatchCommand } from "@/lib/commands";
|
||||
@@ -144,9 +145,9 @@ export function useKeyframeDrag({
|
||||
|
||||
if (!dragState.isDragging) return;
|
||||
|
||||
const startX = mouseDownXRef.current ?? clientX;
|
||||
const rawDelta = (clientX - startX) / pixelsPerSecond;
|
||||
const snappedDelta = snapTimeToFrame({ time: rawDelta, fps });
|
||||
const startX = mouseDownXRef.current ?? clientX;
|
||||
const rawDelta = Math.round(((clientX - startX) / pixelsPerSecond) * TICKS_PER_SECOND);
|
||||
const snappedDelta = roundToFrame({ time: rawDelta, rate: fps }) ?? rawDelta;
|
||||
|
||||
setDragState((previous) => ({ ...previous, deltaTime: snappedDelta }));
|
||||
};
|
||||
@@ -255,7 +256,7 @@ export function useKeyframeDrag({
|
||||
if (wasDrag) return;
|
||||
|
||||
const duration = editor.timeline.getTotalDuration();
|
||||
const seekTime = getSnappedSeekTime({ rawTime: displayedStartTime + indicatorTime, duration, fps });
|
||||
const seekTime = snappedSeekTime({ time: displayedStartTime + indicatorTime, duration, rate: fps }) ?? displayedStartTime + indicatorTime;
|
||||
editor.playback.seek({ time: seekTime });
|
||||
|
||||
if (event.shiftKey) {
|
||||
|
||||
Reference in New Issue
Block a user