OpenCut/apps/web/src/commands/timeline/clipboard/paste-keyframes.ts

136 lines
3.3 KiB
TypeScript

import { EditorCore } from "@/core";
import {
getKeyframeAtTime,
updateScalarKeyframeCurve,
upsertPathKeyframe,
} from "@/animation";
import { Command, type CommandResult } from "@/commands/base-command";
import type { KeyframeClipboardItem } from "@/clipboard";
import type { SceneTracks, TimelineElement } from "@/timeline";
import { updateElementInSceneTracks } from "@/timeline";
import { resolveAnimationTarget } from "@/timeline/animation-targets";
import { generateUUID } from "@/utils/id";
function pasteKeyframesIntoElement({
element,
time,
clipboardItems,
}: {
element: TimelineElement;
time: number;
clipboardItems: KeyframeClipboardItem[];
}): TimelineElement {
let nextElement = element;
for (const item of clipboardItems) {
const target = resolveAnimationTarget({
element: nextElement,
path: item.propertyPath,
});
if (!target) {
continue;
}
const keyframeTime = Math.max(
0,
Math.min(time + item.timeOffset, nextElement.duration),
);
const nextAnimations = upsertPathKeyframe({
animations: nextElement.animations,
propertyPath: item.propertyPath,
time: keyframeTime,
value: item.value,
interpolation: item.interpolation,
keyframeId: generateUUID(),
kind: target.kind,
defaultInterpolation: target.defaultInterpolation,
coerceValue: target.coerceValue,
});
const pastedKeyframe = getKeyframeAtTime({
animations: nextAnimations,
propertyPath: item.propertyPath,
time: keyframeTime,
});
let patchedAnimations = nextAnimations;
if (pastedKeyframe) {
for (const curvePatch of item.curvePatches) {
const nextPatchedAnimations = updateScalarKeyframeCurve({
animations: patchedAnimations,
propertyPath: item.propertyPath,
componentKey: curvePatch.componentKey,
keyframeId: pastedKeyframe.id,
patch: curvePatch.patch,
});
patchedAnimations = nextPatchedAnimations ?? patchedAnimations;
}
}
nextElement = {
...nextElement,
animations: patchedAnimations,
};
}
return nextElement;
}
export class PasteKeyframesCommand extends Command {
private savedState: SceneTracks | null = null;
private readonly trackId: string;
private readonly elementId: string;
private readonly time: number;
private readonly clipboardItems: KeyframeClipboardItem[];
constructor({
trackId,
elementId,
time,
clipboardItems,
}: {
trackId: string;
elementId: string;
time: number;
clipboardItems: KeyframeClipboardItem[];
}) {
super();
this.trackId = trackId;
this.elementId = elementId;
this.time = time;
this.clipboardItems = clipboardItems;
}
execute(): CommandResult | undefined {
if (this.clipboardItems.length === 0) {
return undefined;
}
const editor = EditorCore.getInstance();
this.savedState = editor.scenes.getActiveScene().tracks;
const updatedTracks = updateElementInSceneTracks({
tracks: this.savedState,
trackId: this.trackId,
elementId: this.elementId,
update: (element) =>
pasteKeyframesIntoElement({
element,
time: this.time,
clipboardItems: this.clipboardItems,
}),
});
editor.timeline.updateTracks(updatedTracks);
return undefined;
}
undo(): void {
if (!this.savedState) {
return;
}
const editor = EditorCore.getInstance();
editor.timeline.updateTracks(this.savedState);
}
}