diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-04-13 04:40:56 +0200 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-04-13 05:03:10 +0200 |
| commit | 2272598d420cf078aaa3928246bfa3a414f6eddb (patch) | |
| tree | b52460a2e3576c9e517ef8a9dd86959a68f31b69 /apps/web/src/lib/commands/timeline/clipboard | |
| parent | 5a2aaf3601b6b050603f51e487a31fb07862a21f (diff) | |
| download | opencut-2272598d420cf078aaa3928246bfa3a414f6eddb.tar.gz | |
feat: clipboard with keyframe paste
Made-with: Cursor
Diffstat (limited to 'apps/web/src/lib/commands/timeline/clipboard')
3 files changed, 144 insertions, 11 deletions
diff --git a/apps/web/src/lib/commands/timeline/clipboard/index.ts b/apps/web/src/lib/commands/timeline/clipboard/index.ts index 11a8bed4..e893b62b 100644 --- a/apps/web/src/lib/commands/timeline/clipboard/index.ts +++ b/apps/web/src/lib/commands/timeline/clipboard/index.ts @@ -1 +1,2 @@ export { PasteCommand } from "./paste"; +export { PasteKeyframesCommand } from "./paste-keyframes"; diff --git a/apps/web/src/lib/commands/timeline/clipboard/paste-keyframes.ts b/apps/web/src/lib/commands/timeline/clipboard/paste-keyframes.ts new file mode 100644 index 00000000..210afbd3 --- /dev/null +++ b/apps/web/src/lib/commands/timeline/clipboard/paste-keyframes.ts @@ -0,0 +1,135 @@ +import { EditorCore } from "@/core"; +import { + getKeyframeAtTime, + resolveAnimationTarget, + updateScalarKeyframeCurve, + upsertPathKeyframe, +} from "@/lib/animation"; +import { Command, type CommandResult } from "@/lib/commands/base-command"; +import type { KeyframeClipboardItem } from "@/lib/clipboard"; +import type { SceneTracks, TimelineElement } from "@/lib/timeline"; +import { updateElementInSceneTracks } from "@/lib/timeline"; +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); + } +} diff --git a/apps/web/src/lib/commands/timeline/clipboard/paste.ts b/apps/web/src/lib/commands/timeline/clipboard/paste.ts index 6ccf96fb..347c4f68 100644 --- a/apps/web/src/lib/commands/timeline/clipboard/paste.ts +++ b/apps/web/src/lib/commands/timeline/clipboard/paste.ts @@ -1,10 +1,7 @@ import { Command, type CommandResult } from "@/lib/commands/base-command"; import { EditorCore } from "@/core"; -import type { - SceneTracks, - TimelineElement, - ClipboardItem, -} from "@/lib/timeline"; +import type { SceneTracks, TimelineElement } from "@/lib/timeline"; +import type { ElementClipboardItem } from "@/lib/clipboard"; import { generateUUID } from "@/utils/id"; import { applyPlacement, resolveTrackPlacement, enforceMainTrackStart } from "@/lib/timeline/placement"; import { cloneAnimations } from "@/lib/animation"; @@ -13,14 +10,14 @@ export class PasteCommand extends Command { private savedState: SceneTracks | null = null; private pastedElements: { trackId: string; elementId: string }[] = []; private readonly time: number; - private readonly clipboardItems: ClipboardItem[]; + private readonly clipboardItems: ElementClipboardItem[]; constructor({ time, clipboardItems, }: { time: number; - clipboardItems: ClipboardItem[]; + clipboardItems: ElementClipboardItem[]; }) { super(); this.time = time; @@ -145,9 +142,9 @@ export class PasteCommand extends Command { function groupClipboardItemsByTrackId({ clipboardItems, }: { - clipboardItems: ClipboardItem[]; -}): Map<string, ClipboardItem[]> { - const groupedItems = new Map<string, ClipboardItem[]>(); + clipboardItems: ElementClipboardItem[]; +}): Map<string, ElementClipboardItem[]> { + const groupedItems = new Map<string, ElementClipboardItem[]>(); for (const item of clipboardItems) { const existingItems = groupedItems.get(item.trackId) ?? []; @@ -162,7 +159,7 @@ function buildPastedElements({ minStart, time, }: { - items: ClipboardItem[]; + items: ElementClipboardItem[]; minStart: number; time: number; }): TimelineElement[] { |
