feat(timeline): 支持拖拽调整时长与运镜/轨迹关键帧时间

This commit is contained in:
tongtong committed 2026-09-17 17:30:22 +08:00
1 parent bab92c05b8
commit 8d67f17a84
21 files changed
+2556 -194

No files matched your search

+87 -3
View File
@@ -37,11 +37,18 @@ import {
removeCameraKeyframe as removeTimelineCameraKeyframe,
updateCameraKeyframeTime as updateTimelineCameraKeyframeTime,
updateCameraKeyframeValue as updateTimelineCameraKeyframeValue,
setCameraKeyframeTimes as setTimelineCameraKeyframeTimes,
updateTimelineDuration,
updateTimelinePlaybackSettings,
normalizeTimelineDuration,
normalizeTimelineFps,
DEFAULT_TIMELINE_DURATION,
createKeyframeId,
type KeyframeTransformValue,
type CameraKeyframeValue,
type Timeline,
} from "../schema/timeline";
import { getTrajectoryStartTime, shiftTrajectoryClip } from "../schema/trajectoryMotion";
export type TransformMode = "translate" | "rotate" | "scale";
@@ -141,8 +148,12 @@ export interface DirectorActions {
updateTrajectoryPoint: (
objectId: string,
pointId: string,
patch: Partial<Pick<DirectorTrajectoryPoint, "position" | "rotation">>
patch: Partial<Pick<DirectorTrajectoryPoint, "position" | "rotation" | "time">>
) => void;
/** 一次性覆盖整条轨迹的所有关键帧时间(拖动「轨迹时长」把手时按比例缩放用) */
setTrajectoryPointTimes: (objectId: string, times: number[]) => void;
/** 整体平移人物轨迹片段(拖轨迹条本体):片段起点 + 所有轨迹点时间同步平移 */
setTrajectoryClipStart: (objectId: string, startTime: number) => void;
removeTrajectoryPoint: (objectId: string, pointId: string) => void;
selectTrajectoryPoint: (objectId: string, pointId: string) => void;
updateScene: (patch: Partial<SceneSettings>) => void;
@@ -203,9 +214,13 @@ export interface DirectorActions {
removeCameraKeyframe: (cameraId: string, keyframeId: string) => void;
updateCameraKeyframeTime: (cameraId: string, keyframeId: string, time: number) => void;
updateCameraKeyframeValue: (cameraId: string, keyframeId: string, value: CameraKeyframeValue) => void;
/** 一次性覆盖整条运镜的所有关键帧时间(拖动「运镜时长」把手按比例缩放用) */
setCameraKeyframeTimes: (cameraId: string, times: number[]) => void;
applyCameraKeyframeToShot: (cameraId: string, value: CameraKeyframeValue) => void;
// 时长
setTimelineDuration: (seconds: number) => void;
/** 时间轴播放设置(每秒帧数 / 是否循环):设置弹窗里点「完成」时一次性写入 */
setTimelinePlaybackSettings: (patch: { fps?: number; loop?: boolean }) => void;
clearAllKeyframes: () => void;
beginUndoBatch: () => void;
endUndoBatch: () => void;
@@ -443,13 +458,21 @@ function withPersistedLocalAssets(project: DirectorProject, includePersistedLoca
function withTimeline(project: DirectorProject): DirectorProject {
const timeline = project.timeline ?? createDefaultTimeline();
// 旧工程里没有 fps / loop:补默认值(30fps、不循环),duration 也顺手夹一次
const duration =
typeof timeline.duration === "number" && Number.isFinite(timeline.duration)
? timeline.duration
: DEFAULT_TIMELINE_DURATION;
return {
...project,
version: 2,
timeline: {
duration: timeline.duration,
duration: normalizeTimelineDuration(duration),
tracks: timeline.tracks ?? [],
cameraTracks: timeline.cameraTracks ?? [],
fps: normalizeTimelineFps(timeline.fps),
loop: timeline.loop === true,
},
};
}
@@ -1674,12 +1697,57 @@ export const useDirectorStore = create<DirectorStore>((set, get) => {
normalizeTrajectoryRotation(2, patch.rotation[2]),
] as [number, number, number])
: point.rotation,
// 关键帧时间:拖到哪儿就是哪儿(合法性由拖动时的夹取保证)
time:
typeof patch.time === "number" && Number.isFinite(patch.time)
? patch.time
: point.time,
}
: point
),
})),
},
})),
setTrajectoryPointTimes: (objectId, times) =>
commitMutation((state) => ({
...state,
project: {
...state.project,
objects: updateObjectById(state.project.objects, objectId, (item) => ({
...item,
trajectoryPoints: (item.trajectoryPoints ?? []).map((point, index) => {
const time = times[index];
return typeof time === "number" && Number.isFinite(time) ? { ...point, time } : point;
}),
})),
},
})),
setTrajectoryClipStart: (objectId, startTime) =>
commitMutation((state) => ({
...state,
project: {
...state.project,
objects: updateObjectById(state.project.objects, objectId, (item) => {
const points = item.trajectoryPoints ?? [];
if (points.length === 0) return item;
// 片段刚性平移:起点夹在时间轴内,所有轨迹点跟着走同样的量。
// 点的时间在这里落成显式值,避免默认推导把片段「压缩」而不是「平移」。
const { startTime: nextStart, times } = shiftTrajectoryClip(
points,
state.project.timeline.duration,
getTrajectoryStartTime(item, state.project.timeline.duration),
startTime
);
return {
...item,
trajectoryStartTime: nextStart,
trajectoryPoints: points.map((point, index) => ({ ...point, time: times[index] })),
};
}),
},
})),
removeTrajectoryPoint: (objectId, pointId) =>
commitMutation((state) => ({
...state,
@@ -2568,6 +2636,14 @@ export const useDirectorStore = create<DirectorStore>((set, get) => {
timeline: updateTimelineCameraKeyframeValue(state.project.timeline, cameraId, keyframeId, value),
},
})),
setCameraKeyframeTimes: (cameraId, times) =>
commitMutation((state) => ({
...state,
project: {
...state.project,
timeline: setTimelineCameraKeyframeTimes(state.project.timeline, cameraId, times),
},
})),
applyCameraKeyframeToShot: (cameraId, value) => {
// 只改 position/target/fov,rotation/scale 保留 shot 原值(相机朝向由 target 决定)
const camera = get().project.cameras.find((item) => item.id === cameraId);
@@ -2583,7 +2659,15 @@ export const useDirectorStore = create<DirectorStore>((set, get) => {
...state,
project: {
...state.project,
timeline: { ...state.project.timeline, duration: Math.max(0.1, seconds) },
timeline: updateTimelineDuration(state.project.timeline, seconds),
},
})),
setTimelinePlaybackSettings: (patch) =>
commitMutation((state) => ({
...state,
project: {
...state.project,
timeline: updateTimelinePlaybackSettings(state.project.timeline, patch),
},
})),
clearAllKeyframes: () =>