mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-24 14:55:37 +00:00
make loop cursor trim-window aware
This commit is contained in:
@@ -53,7 +53,7 @@ import { getAssetPath } from "@/lib/assetPath";
|
||||
import { useShortcuts } from "@/contexts/ShortcutsContext";
|
||||
import { matchesShortcut } from "@/lib/shortcuts";
|
||||
import { detectInteractionCandidates, normalizeCursorTelemetry } from "./timeline/zoomSuggestionUtils";
|
||||
import { buildLoopedCursorTelemetry } from "./videoPlayback/cursorLoopTelemetry";
|
||||
import { buildLoopedCursorTelemetry, getDisplayedTimelineWindowMs } from "./videoPlayback/cursorLoopTelemetry";
|
||||
import { findDominantRegion } from "./videoPlayback/zoomRegionUtils";
|
||||
|
||||
const LOOP_CURSOR_END_WINDOW_MS = 670;
|
||||
@@ -613,39 +613,46 @@ export default function VideoEditor() {
|
||||
return normalizeCursorTelemetry(cursorTelemetry, totalMs > 0 ? totalMs : Number.MAX_SAFE_INTEGER);
|
||||
}, [cursorTelemetry, duration]);
|
||||
|
||||
const displayedTimelineWindow = useMemo(() => {
|
||||
const totalMs = Math.max(0, Math.round(duration * 1000));
|
||||
return getDisplayedTimelineWindowMs(totalMs, trimRegions);
|
||||
}, [duration, trimRegions]);
|
||||
|
||||
const effectiveCursorTelemetry = useMemo(() => {
|
||||
if (!loopCursor) {
|
||||
return normalizedCursorTelemetry;
|
||||
}
|
||||
|
||||
const totalMs = Math.max(0, Math.round(duration * 1000));
|
||||
if (normalizedCursorTelemetry.length < 2 || totalMs <= 0) {
|
||||
if (normalizedCursorTelemetry.length < 2 || displayedTimelineWindow.endMs <= displayedTimelineWindow.startMs) {
|
||||
return normalizedCursorTelemetry;
|
||||
}
|
||||
|
||||
return buildLoopedCursorTelemetry(normalizedCursorTelemetry, totalMs);
|
||||
}, [loopCursor, normalizedCursorTelemetry, duration]);
|
||||
return buildLoopedCursorTelemetry(
|
||||
normalizedCursorTelemetry,
|
||||
displayedTimelineWindow.endMs,
|
||||
displayedTimelineWindow.startMs,
|
||||
);
|
||||
}, [loopCursor, normalizedCursorTelemetry, displayedTimelineWindow]);
|
||||
|
||||
const effectiveZoomRegions = useMemo(() => {
|
||||
if (!loopCursor || zoomRegions.length === 0) {
|
||||
return zoomRegions;
|
||||
}
|
||||
|
||||
const totalMs = Math.max(0, Math.round(duration * 1000));
|
||||
if (totalMs <= 0) {
|
||||
if (displayedTimelineWindow.endMs <= displayedTimelineWindow.startMs) {
|
||||
return zoomRegions;
|
||||
}
|
||||
|
||||
const dominantAtStart = findDominantRegion(zoomRegions, 0, { connectZooms }).region;
|
||||
const dominantAtStart = findDominantRegion(zoomRegions, displayedTimelineWindow.startMs, { connectZooms }).region;
|
||||
if (!dominantAtStart) {
|
||||
return zoomRegions;
|
||||
}
|
||||
|
||||
const endWindowStartMs = Math.max(0, totalMs - LOOP_CURSOR_END_WINDOW_MS);
|
||||
const endWindowStartMs = Math.max(displayedTimelineWindow.startMs, displayedTimelineWindow.endMs - LOOP_CURSOR_END_WINDOW_MS);
|
||||
const loopEndRegion: ZoomRegion = {
|
||||
id: `${dominantAtStart.id}__loop-end-sync`,
|
||||
startMs: endWindowStartMs,
|
||||
endMs: totalMs,
|
||||
endMs: displayedTimelineWindow.endMs,
|
||||
depth: dominantAtStart.depth,
|
||||
focus: {
|
||||
cx: dominantAtStart.focus.cx,
|
||||
@@ -657,7 +664,7 @@ export default function VideoEditor() {
|
||||
...zoomRegions.filter((region) => region.id !== loopEndRegion.id),
|
||||
loopEndRegion,
|
||||
];
|
||||
}, [loopCursor, zoomRegions, duration, connectZooms]);
|
||||
}, [loopCursor, zoomRegions, displayedTimelineWindow, connectZooms]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!videoPath || duration <= 0 || zoomRegions.length > 0 || normalizedCursorTelemetry.length < 2) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { CursorTelemetryPoint } from '../types';
|
||||
import type { CursorTelemetryPoint, TrimRegion } from '../types';
|
||||
|
||||
const LOOP_CURSOR_FREEZE_DURATION_MS = 670;
|
||||
const LOOP_CURSOR_RETURN_STEPS = 20;
|
||||
@@ -141,21 +141,30 @@ function findLatestSample(samples: CursorTelemetryPoint[], timeMs: number) {
|
||||
export function buildLoopedCursorTelemetry(
|
||||
samples: CursorTelemetryPoint[],
|
||||
totalDurationMs: number,
|
||||
timelineStartMs = 0,
|
||||
): CursorTelemetryPoint[] {
|
||||
if (!samples || samples.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const timelineEndMs = Math.max(0, Math.round(totalDurationMs));
|
||||
if (timelineEndMs <= 0) {
|
||||
const timelineStartClampedMs = clamp(Math.round(timelineStartMs), 0, timelineEndMs);
|
||||
if (timelineEndMs <= timelineStartClampedMs) {
|
||||
return samples;
|
||||
}
|
||||
|
||||
const firstSample = samples[0];
|
||||
const lastSample = samples[samples.length - 1];
|
||||
const boundedSamples = samples.filter(
|
||||
(sample) => sample.timeMs >= timelineStartClampedMs && sample.timeMs <= timelineEndMs,
|
||||
);
|
||||
if (boundedSamples.length === 0) {
|
||||
return samples;
|
||||
}
|
||||
|
||||
const firstSample = boundedSamples[0];
|
||||
const lastSample = boundedSamples[boundedSamples.length - 1];
|
||||
const maxFreezeWindowMs = timelineEndMs - firstSample.timeMs;
|
||||
if (maxFreezeWindowMs <= 1) {
|
||||
return samples;
|
||||
return boundedSamples;
|
||||
}
|
||||
|
||||
const freezeDurationMs = Math.min(LOOP_CURSOR_FREEZE_DURATION_MS, maxFreezeWindowMs);
|
||||
@@ -163,10 +172,10 @@ export function buildLoopedCursorTelemetry(
|
||||
const clampedSettleDurationMs = Math.min(LOOP_CURSOR_SETTLE_DURATION_MS, Math.max(0, freezeDurationMs - 1));
|
||||
const returnMotionDurationMs = Math.max(1, freezeDurationMs - clampedSettleDurationMs);
|
||||
const sourceStartMs = firstSample.timeMs;
|
||||
const sourceEndMs = Math.max(sourceStartMs, findLastMovingSampleTime(samples));
|
||||
const sourceEndMs = Math.max(sourceStartMs, findLastMovingSampleTime(boundedSamples));
|
||||
const sourceDurationMs = Math.max(1, sourceEndMs - sourceStartMs);
|
||||
const playbackWindowMs = Math.max(1, motionEndMs);
|
||||
const startingCursorType = findFirstStableCursorType(samples);
|
||||
const startingCursorType = findFirstStableCursorType(boundedSamples);
|
||||
const loopedSamples: CursorTelemetryPoint[] = [
|
||||
{
|
||||
...firstSample,
|
||||
@@ -176,7 +185,7 @@ export function buildLoopedCursorTelemetry(
|
||||
},
|
||||
];
|
||||
|
||||
for (const sample of samples) {
|
||||
for (const sample of boundedSamples) {
|
||||
const progress = clamp((sample.timeMs - sourceStartMs) / sourceDurationMs, 0, 1);
|
||||
const mappedTimeMs = Math.round(playbackWindowMs * progress);
|
||||
|
||||
@@ -230,4 +239,56 @@ export function buildLoopedCursorTelemetry(
|
||||
}
|
||||
|
||||
return loopedSamples;
|
||||
}
|
||||
|
||||
export function getDisplayedTimelineEndMs(totalDurationMs: number, trimRegions: TrimRegion[]) {
|
||||
return getDisplayedTimelineWindowMs(totalDurationMs, trimRegions).endMs;
|
||||
}
|
||||
|
||||
export function getDisplayedTimelineWindowMs(totalDurationMs: number, trimRegions: TrimRegion[]) {
|
||||
const durationMs = Math.max(0, Math.round(totalDurationMs));
|
||||
if (durationMs <= 0) {
|
||||
return { startMs: 0, endMs: 0 };
|
||||
}
|
||||
|
||||
if (!trimRegions || trimRegions.length === 0) {
|
||||
return { startMs: 0, endMs: durationMs };
|
||||
}
|
||||
|
||||
const sortedTrimRegions = trimRegions
|
||||
.map((region) => ({
|
||||
startMs: clamp(Math.round(region.startMs), 0, durationMs),
|
||||
endMs: clamp(Math.round(region.endMs), 0, durationMs),
|
||||
}))
|
||||
.filter((region) => region.endMs > region.startMs)
|
||||
.sort((a, b) => a.startMs - b.startMs);
|
||||
|
||||
if (sortedTrimRegions.length === 0) {
|
||||
return { startMs: 0, endMs: durationMs };
|
||||
}
|
||||
|
||||
let cursorMs = 0;
|
||||
let firstVisibleStartMs: number | null = null;
|
||||
let lastVisibleEndMs = 0;
|
||||
|
||||
for (const trimRegion of sortedTrimRegions) {
|
||||
if (trimRegion.startMs > cursorMs) {
|
||||
if (firstVisibleStartMs === null) {
|
||||
firstVisibleStartMs = cursorMs;
|
||||
}
|
||||
lastVisibleEndMs = trimRegion.startMs;
|
||||
}
|
||||
cursorMs = Math.max(cursorMs, trimRegion.endMs);
|
||||
}
|
||||
|
||||
if (cursorMs < durationMs) {
|
||||
if (firstVisibleStartMs === null) {
|
||||
firstVisibleStartMs = cursorMs;
|
||||
}
|
||||
lastVisibleEndMs = durationMs;
|
||||
}
|
||||
|
||||
const clampedEndMs = clamp(lastVisibleEndMs, 0, durationMs);
|
||||
const clampedStartMs = clamp(firstVisibleStartMs ?? 0, 0, clampedEndMs);
|
||||
return { startMs: clampedStartMs, endMs: clampedEndMs };
|
||||
}
|
||||
Reference in New Issue
Block a user