mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
feat: masks, properties refactor, shaders, storage migrations, and more
This commit is contained in:
@@ -1,16 +1,17 @@
|
||||
import type { EditorCore } from "@/core";
|
||||
import type { EffectParamValues } from "@/types/effects";
|
||||
import type { ParamValues } from "@/lib/params";
|
||||
import type {
|
||||
TrackType,
|
||||
TimelineTrack,
|
||||
TimelineElement,
|
||||
ClipboardItem,
|
||||
} from "@/types/timeline";
|
||||
RetimeConfig,
|
||||
} from "@/lib/timeline";
|
||||
import type {
|
||||
AnimationPath,
|
||||
AnimationInterpolation,
|
||||
AnimationPropertyPath,
|
||||
AnimationValue,
|
||||
} from "@/types/animation";
|
||||
} from "@/lib/animation/types";
|
||||
import { calculateTotalDuration } from "@/lib/timeline";
|
||||
import {
|
||||
AddTrackCommand,
|
||||
@@ -30,6 +31,7 @@ import {
|
||||
UpdateElementStartTimeCommand,
|
||||
MoveElementCommand,
|
||||
TracksSnapshotCommand,
|
||||
UpdateElementRetimeCommand,
|
||||
UpsertKeyframeCommand,
|
||||
RemoveKeyframeCommand,
|
||||
RetimeKeyframeCommand,
|
||||
@@ -38,15 +40,18 @@ import {
|
||||
UpdateClipEffectParamsCommand,
|
||||
ToggleClipEffectCommand,
|
||||
ReorderClipEffectsCommand,
|
||||
RemoveMaskCommand,
|
||||
ToggleMaskInvertedCommand,
|
||||
UpsertEffectParamKeyframeCommand,
|
||||
RemoveEffectParamKeyframeCommand,
|
||||
} from "@/lib/commands/timeline";
|
||||
import { BatchCommand, PreviewTracker } from "@/lib/commands";
|
||||
import { BatchCommand } from "@/lib/commands";
|
||||
import type { InsertElementParams } from "@/lib/commands/timeline/element/insert-element";
|
||||
|
||||
export class TimelineManager {
|
||||
private listeners = new Set<() => void>();
|
||||
private previewTracker = new PreviewTracker<TimelineTrack[]>();
|
||||
private previewOverlay = new Map<string, Partial<TimelineElement>>();
|
||||
private previewTracks: TimelineTrack[] | null = null;
|
||||
|
||||
constructor(private editor: EditorCore) {}
|
||||
|
||||
@@ -121,6 +126,29 @@ export class TimelineManager {
|
||||
}
|
||||
}
|
||||
|
||||
updateElementRetime({
|
||||
trackId,
|
||||
elementId,
|
||||
retime,
|
||||
pushHistory = true,
|
||||
}: {
|
||||
trackId: string;
|
||||
elementId: string;
|
||||
retime?: RetimeConfig;
|
||||
pushHistory?: boolean;
|
||||
}): void {
|
||||
const command = new UpdateElementRetimeCommand({
|
||||
trackId,
|
||||
elementId,
|
||||
retime,
|
||||
});
|
||||
if (pushHistory) {
|
||||
this.editor.command.execute({ command });
|
||||
} else {
|
||||
command.execute();
|
||||
}
|
||||
}
|
||||
|
||||
updateElementStartTime({
|
||||
elements,
|
||||
startTime,
|
||||
@@ -308,6 +336,23 @@ export class TimelineManager {
|
||||
this.editor.command.execute({ command });
|
||||
}
|
||||
|
||||
removeMask({
|
||||
trackId,
|
||||
elementId,
|
||||
maskId,
|
||||
}: {
|
||||
trackId: string;
|
||||
elementId: string;
|
||||
maskId: string;
|
||||
}): void {
|
||||
const command = new RemoveMaskCommand({
|
||||
trackId,
|
||||
elementId,
|
||||
maskId,
|
||||
});
|
||||
this.editor.command.execute({ command });
|
||||
}
|
||||
|
||||
updateClipEffectParams({
|
||||
trackId,
|
||||
elementId,
|
||||
@@ -318,7 +363,7 @@ export class TimelineManager {
|
||||
trackId: string;
|
||||
elementId: string;
|
||||
effectId: string;
|
||||
params: Partial<EffectParamValues>;
|
||||
params: Partial<ParamValues>;
|
||||
pushHistory?: boolean;
|
||||
}): void {
|
||||
const command = new UpdateClipEffectParamsCommand({
|
||||
@@ -351,6 +396,23 @@ export class TimelineManager {
|
||||
this.editor.command.execute({ command });
|
||||
}
|
||||
|
||||
toggleMaskInverted({
|
||||
trackId,
|
||||
elementId,
|
||||
maskId,
|
||||
}: {
|
||||
trackId: string;
|
||||
elementId: string;
|
||||
maskId: string;
|
||||
}): void {
|
||||
const command = new ToggleMaskInvertedCommand({
|
||||
trackId,
|
||||
elementId,
|
||||
maskId,
|
||||
});
|
||||
this.editor.command.execute({ command });
|
||||
}
|
||||
|
||||
reorderClipEffects({
|
||||
trackId,
|
||||
elementId,
|
||||
@@ -377,7 +439,7 @@ export class TimelineManager {
|
||||
keyframes: Array<{
|
||||
trackId: string;
|
||||
elementId: string;
|
||||
propertyPath: AnimationPropertyPath;
|
||||
propertyPath: AnimationPath;
|
||||
time: number;
|
||||
value: AnimationValue;
|
||||
interpolation?: AnimationInterpolation;
|
||||
@@ -419,7 +481,7 @@ export class TimelineManager {
|
||||
keyframes: Array<{
|
||||
trackId: string;
|
||||
elementId: string;
|
||||
propertyPath: AnimationPropertyPath;
|
||||
propertyPath: AnimationPath;
|
||||
keyframeId: string;
|
||||
}>;
|
||||
}): void {
|
||||
@@ -450,7 +512,7 @@ export class TimelineManager {
|
||||
}: {
|
||||
trackId: string;
|
||||
elementId: string;
|
||||
propertyPath: AnimationPropertyPath;
|
||||
propertyPath: AnimationPath;
|
||||
keyframeId: string;
|
||||
time: number;
|
||||
}): void {
|
||||
@@ -520,7 +582,7 @@ export class TimelineManager {
|
||||
}
|
||||
|
||||
isPreviewActive(): boolean {
|
||||
return this.previewTracker.isActive();
|
||||
return this.previewOverlay.size > 0;
|
||||
}
|
||||
|
||||
previewElements({
|
||||
@@ -532,37 +594,51 @@ export class TimelineManager {
|
||||
updates: Partial<TimelineElement>;
|
||||
}>;
|
||||
}): void {
|
||||
const tracks = this.getTracks();
|
||||
this.previewTracker.begin({ state: tracks });
|
||||
|
||||
let updatedTracks = tracks;
|
||||
for (const { trackId, elementId, updates: elementUpdates } of updates) {
|
||||
updatedTracks = updatedTracks.map((track) => {
|
||||
if (track.id !== trackId) return track;
|
||||
const newElements = track.elements.map((element) =>
|
||||
element.id === elementId
|
||||
? { ...element, ...elementUpdates }
|
||||
: element,
|
||||
);
|
||||
return { ...track, elements: newElements } as TimelineTrack;
|
||||
});
|
||||
for (const { elementId, updates: elementUpdates } of updates) {
|
||||
const existingOverlay = this.previewOverlay.get(elementId);
|
||||
const mergedOverlay = {
|
||||
...existingOverlay,
|
||||
...elementUpdates,
|
||||
} as Partial<TimelineElement>;
|
||||
this.previewOverlay.set(elementId, mergedOverlay);
|
||||
}
|
||||
this.updateTracks(updatedTracks);
|
||||
const committedTracks = this.editor.scenes.getActiveScene()?.tracks ?? [];
|
||||
this.previewTracks = this.applyPreviewOverlay(committedTracks);
|
||||
this.notify();
|
||||
}
|
||||
|
||||
commitPreview(): void {
|
||||
const snapshot = this.previewTracker.end();
|
||||
if (snapshot === null) return;
|
||||
const currentTracks = this.getTracks();
|
||||
const command = new TracksSnapshotCommand(snapshot, currentTracks);
|
||||
if (this.previewOverlay.size === 0) return;
|
||||
const committedTracks = this.editor.scenes.getActiveScene()?.tracks ?? [];
|
||||
const afterTracks =
|
||||
this.previewTracks ?? this.applyPreviewOverlay(committedTracks);
|
||||
const command = new TracksSnapshotCommand(committedTracks, afterTracks);
|
||||
this.editor.command.push({ command });
|
||||
this.previewOverlay.clear();
|
||||
this.previewTracks = null;
|
||||
this.updateTracks(afterTracks);
|
||||
}
|
||||
|
||||
discardPreview(): void {
|
||||
const snapshot = this.previewTracker.end();
|
||||
if (snapshot !== null) {
|
||||
this.updateTracks(snapshot);
|
||||
}
|
||||
if (this.previewOverlay.size === 0) return;
|
||||
this.previewOverlay.clear();
|
||||
this.previewTracks = null;
|
||||
this.notify();
|
||||
}
|
||||
|
||||
private applyPreviewOverlay(tracks: TimelineTrack[]): TimelineTrack[] {
|
||||
if (this.previewOverlay.size === 0) return tracks;
|
||||
return tracks.map((track) => {
|
||||
const hasOverlay = track.elements.some((el) =>
|
||||
this.previewOverlay.has(el.id),
|
||||
);
|
||||
if (!hasOverlay) return track;
|
||||
const newElements = track.elements.map((el) => {
|
||||
const overlay = this.previewOverlay.get(el.id);
|
||||
return overlay ? ({ ...el, ...overlay } as TimelineElement) : el;
|
||||
});
|
||||
return { ...track, elements: newElements } as TimelineTrack;
|
||||
});
|
||||
}
|
||||
|
||||
duplicateElements({
|
||||
@@ -597,16 +673,25 @@ export class TimelineManager {
|
||||
return this.editor.scenes.getActiveScene()?.tracks ?? [];
|
||||
}
|
||||
|
||||
getRenderTracks(): TimelineTrack[] {
|
||||
if (this.previewTracks !== null) return this.previewTracks;
|
||||
return this.getTracks();
|
||||
}
|
||||
|
||||
subscribe(listener: () => void): () => void {
|
||||
this.listeners.add(listener);
|
||||
return () => this.listeners.delete(listener);
|
||||
}
|
||||
|
||||
private notify(): void {
|
||||
this.listeners.forEach((fn) => fn());
|
||||
this.listeners.forEach((fn) => {
|
||||
fn();
|
||||
});
|
||||
}
|
||||
|
||||
updateTracks(newTracks: TimelineTrack[]): void {
|
||||
this.previewOverlay.clear();
|
||||
this.previewTracks = null;
|
||||
this.editor.scenes.updateSceneTracks({ tracks: newTracks });
|
||||
this.notify();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user