feat: masks, properties refactor, shaders, storage migrations, and more

This commit is contained in:
Maze Winther
2026-03-29 15:48:22 +02:00
parent 39ea298a9c
commit 8db3bead13
690 changed files with 35618 additions and 7337 deletions
+120 -35
View File
@@ -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();
}