diff options
Diffstat (limited to 'apps/web/src/lib')
| -rw-r--r-- | apps/web/src/lib/actions/definitions.ts | 4 | ||||
| -rw-r--r-- | apps/web/src/lib/commands/timeline/element/delete-elements.ts | 51 | ||||
| -rw-r--r-- | apps/web/src/lib/commands/timeline/element/split-elements.ts | 149 | ||||
| -rw-r--r-- | apps/web/src/lib/timeline/index.ts | 1 | ||||
| -rw-r--r-- | apps/web/src/lib/timeline/ripple-utils.ts | 17 |
5 files changed, 142 insertions, 80 deletions
diff --git a/apps/web/src/lib/actions/definitions.ts b/apps/web/src/lib/actions/definitions.ts index 83144a25..5ec056c4 100644 --- a/apps/web/src/lib/actions/definitions.ts +++ b/apps/web/src/lib/actions/definitions.ts @@ -105,6 +105,10 @@ export const ACTIONS = { category: "editing", defaultShortcuts: ["n"], }, + "toggle-ripple-editing": { + description: "Toggle ripple editing", + category: "editing", + }, "select-all": { description: "Select all elements", category: "selection", diff --git a/apps/web/src/lib/commands/timeline/element/delete-elements.ts b/apps/web/src/lib/commands/timeline/element/delete-elements.ts index 3d1e50cc..7fb46c36 100644 --- a/apps/web/src/lib/commands/timeline/element/delete-elements.ts +++ b/apps/web/src/lib/commands/timeline/element/delete-elements.ts @@ -1,19 +1,23 @@ import { Command } from "@/lib/commands/base-command"; import type { TimelineTrack } from "@/types/timeline"; import { EditorCore } from "@/core"; -import { isMainTrack } from "@/lib/timeline"; +import { isMainTrack, rippleShiftElements } from "@/lib/timeline"; export class DeleteElementsCommand extends Command { private savedState: TimelineTrack[] | null = null; private readonly elements: { trackId: string; elementId: string }[]; + private readonly rippleEnabled: boolean; constructor({ elements, + rippleEnabled = false, }: { elements: { trackId: string; elementId: string }[]; + rippleEnabled?: boolean; }) { super(); this.elements = elements; + this.rippleEnabled = rippleEnabled; } execute(): void { @@ -22,25 +26,44 @@ export class DeleteElementsCommand extends Command { const updatedTracks = this.savedState .map((track) => { - const hasElementsToDelete = this.elements.some( - (elementEntry) => elementEntry.trackId === track.id, + const elementsToDeleteOnTrack = this.elements.filter( + (target) => target.trackId === track.id, ); + const hasElementsToDelete = elementsToDeleteOnTrack.length > 0; if (!hasElementsToDelete) { return track; } - return { - ...track, - elements: track.elements.filter( - (element) => - !this.elements.some( - (elementEntry) => - elementEntry.trackId === track.id && - elementEntry.elementId === element.id, - ), - ), - } as typeof track; + const deletedElementInfos = elementsToDeleteOnTrack + .map((target) => + track.elements.find((element) => element.id === target.elementId), + ) + .filter((element): element is NonNullable<typeof element> => element !== undefined) + .map((element) => ({ startTime: element.startTime, duration: element.duration })); + + let elements = track.elements.filter( + (element) => + !this.elements.some( + (target) => + target.trackId === track.id && target.elementId === element.id, + ), + ); + + if (this.rippleEnabled && deletedElementInfos.length > 0) { + const sortedByStartDesc = [...deletedElementInfos].sort( + (a, b) => b.startTime - a.startTime, + ); + for (const { startTime, duration } of sortedByStartDesc) { + elements = rippleShiftElements({ + elements, + afterTime: startTime, + shiftAmount: duration, + }); + } + } + + return { ...track, elements } as typeof track; }) .filter((track) => track.elements.length > 0 || isMainTrack(track)); diff --git a/apps/web/src/lib/commands/timeline/element/split-elements.ts b/apps/web/src/lib/commands/timeline/element/split-elements.ts index be89e2ff..37ccbb12 100644 --- a/apps/web/src/lib/commands/timeline/element/split-elements.ts +++ b/apps/web/src/lib/commands/timeline/element/split-elements.ts @@ -2,6 +2,7 @@ import { Command } from "@/lib/commands/base-command"; import type { TimelineTrack } from "@/types/timeline"; import { generateUUID } from "@/utils/id"; import { EditorCore } from "@/core"; +import { rippleShiftElements } from "@/lib/timeline"; import { splitAnimationsAtTime } from "@/lib/animation"; export class SplitElementsCommand extends Command { @@ -11,20 +12,24 @@ export class SplitElementsCommand extends Command { private readonly elements: { trackId: string; elementId: string }[]; private readonly splitTime: number; private readonly retainSide: "both" | "left" | "right"; + private readonly rippleEnabled: boolean; constructor({ elements, splitTime, retainSide = "both", + rippleEnabled = false, }: { elements: { trackId: string; elementId: string }[]; splitTime: number; retainSide?: "both" | "left" | "right"; + rippleEnabled?: boolean; }) { super(); this.elements = elements; this.splitTime = splitTime; this.retainSide = retainSide; + this.rippleEnabled = rippleEnabled; } getRightSideElements(): { trackId: string; elementId: string }[] { @@ -39,81 +44,44 @@ export class SplitElementsCommand extends Command { const updatedTracks = this.savedState.map((track) => { const elementsToSplit = this.elements.filter( - (elementEntry) => elementEntry.trackId === track.id, + (target) => target.trackId === track.id, ); if (elementsToSplit.length === 0) { return track; } - return { - ...track, - elements: track.elements.flatMap((element) => { - const shouldSplit = elementsToSplit.some( - (elementEntry) => elementEntry.elementId === element.id, - ); + let leftVisibleDurationForRipple: number | null = null; - if (!shouldSplit) { - return [element]; - } - - const effectiveStart = element.startTime; - const effectiveEnd = element.startTime + element.duration; - - if ( - this.splitTime <= effectiveStart || - this.splitTime >= effectiveEnd - ) { - return [element]; - } + let elements = track.elements.flatMap((element) => { + const shouldSplit = elementsToSplit.some( + (target) => target.elementId === element.id, + ); - const relativeTime = this.splitTime - element.startTime; - const leftVisibleDuration = relativeTime; - const rightVisibleDuration = element.duration - relativeTime; - const { leftAnimations, rightAnimations } = splitAnimationsAtTime({ - animations: element.animations, - splitTime: relativeTime, - shouldIncludeSplitBoundary: true, - }); + if (!shouldSplit) { + return [element]; + } - if (this.retainSide === "left") { - return [ - { - ...element, - duration: leftVisibleDuration, - trimEnd: element.trimEnd + rightVisibleDuration, - name: `${element.name} (left)`, - animations: leftAnimations, - }, - ]; - } + const effectiveStart = element.startTime; + const effectiveEnd = element.startTime + element.duration; - if (this.retainSide === "right") { - const newId = generateUUID(); - this.rightSideElements.push({ - trackId: track.id, - elementId: newId, - }); - return [ - { - ...element, - id: newId, - startTime: this.splitTime, - duration: rightVisibleDuration, - trimStart: element.trimStart + leftVisibleDuration, - name: `${element.name} (right)`, - animations: rightAnimations, - }, - ]; - } + if ( + this.splitTime <= effectiveStart || + this.splitTime >= effectiveEnd + ) { + return [element]; + } - // "both" - split into two pieces - const secondElementId = generateUUID(); - this.rightSideElements.push({ - trackId: track.id, - elementId: secondElementId, - }); + const relativeTime = this.splitTime - element.startTime; + const leftVisibleDuration = relativeTime; + const rightVisibleDuration = element.duration - relativeTime; + const { leftAnimations, rightAnimations } = splitAnimationsAtTime({ + animations: element.animations, + splitTime: relativeTime, + shouldIncludeSplitBoundary: true, + }); + if (this.retainSide === "left") { return [ { ...element, @@ -122,9 +90,22 @@ export class SplitElementsCommand extends Command { name: `${element.name} (left)`, animations: leftAnimations, }, + ]; + } + + if (this.retainSide === "right") { + if (this.rippleEnabled && elementsToSplit.length === 1) { + leftVisibleDurationForRipple = leftVisibleDuration; + } + const newId = generateUUID(); + this.rightSideElements.push({ + trackId: track.id, + elementId: newId, + }); + return [ { ...element, - id: secondElementId, + id: newId, startTime: this.splitTime, duration: rightVisibleDuration, trimStart: element.trimStart + leftVisibleDuration, @@ -132,8 +113,44 @@ export class SplitElementsCommand extends Command { animations: rightAnimations, }, ]; - }), - } as typeof track; + } + + // "both" - split into two pieces + const secondElementId = generateUUID(); + this.rightSideElements.push({ + trackId: track.id, + elementId: secondElementId, + }); + + return [ + { + ...element, + duration: leftVisibleDuration, + trimEnd: element.trimEnd + rightVisibleDuration, + name: `${element.name} (left)`, + animations: leftAnimations, + }, + { + ...element, + id: secondElementId, + startTime: this.splitTime, + duration: rightVisibleDuration, + trimStart: element.trimStart + leftVisibleDuration, + name: `${element.name} (right)`, + animations: rightAnimations, + }, + ]; + }); + + if (this.rippleEnabled && leftVisibleDurationForRipple !== null) { + elements = rippleShiftElements({ + elements, + afterTime: this.splitTime, + shiftAmount: leftVisibleDurationForRipple, + }); + } + + return { ...track, elements } as typeof track; }); editor.timeline.updateTracks(updatedTracks); diff --git a/apps/web/src/lib/timeline/index.ts b/apps/web/src/lib/timeline/index.ts index c7236887..887ff7ab 100644 --- a/apps/web/src/lib/timeline/index.ts +++ b/apps/web/src/lib/timeline/index.ts @@ -5,6 +5,7 @@ export * from "./track-element-update"; export * from "./element-utils"; export * from "./zoom-utils"; export * from "./ruler-utils"; +export * from "./ripple-utils"; export * from "./pixel-utils"; export function calculateTotalDuration({ diff --git a/apps/web/src/lib/timeline/ripple-utils.ts b/apps/web/src/lib/timeline/ripple-utils.ts new file mode 100644 index 00000000..5f37bf29 --- /dev/null +++ b/apps/web/src/lib/timeline/ripple-utils.ts @@ -0,0 +1,17 @@ +import type { TimelineElement } from "@/types/timeline"; + +export function rippleShiftElements({ + elements, + afterTime, + shiftAmount, +}: { + elements: TimelineElement[]; + afterTime: number; + shiftAmount: number; +}): TimelineElement[] { + return elements.map((element) => + element.startTime >= afterTime + ? { ...element, startTime: element.startTime - shiftAmount } + : element, + ); +} |
