From db50b5214de28b9ffc02d0d80623cde13794c5ef Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Mon, 2 Mar 2026 13:26:40 +0100 Subject: feat: ripple support for element resize --- apps/web/src/core/managers/timeline-manager.ts | 3 + .../hooks/timeline/element/use-element-resize.ts | 6 +- .../timeline/element/update-element-trim.ts | 66 ++++++++++++++++------ 3 files changed, 57 insertions(+), 18 deletions(-) (limited to 'apps/web/src') diff --git a/apps/web/src/core/managers/timeline-manager.ts b/apps/web/src/core/managers/timeline-manager.ts index 53007cf9..173f0744 100644 --- a/apps/web/src/core/managers/timeline-manager.ts +++ b/apps/web/src/core/managers/timeline-manager.ts @@ -73,6 +73,7 @@ export class TimelineManager { startTime, duration, pushHistory = true, + rippleEnabled = false, }: { elementId: string; trimStart: number; @@ -80,6 +81,7 @@ export class TimelineManager { startTime?: number; duration?: number; pushHistory?: boolean; + rippleEnabled?: boolean; }): void { const command = new UpdateElementTrimCommand({ elementId, @@ -87,6 +89,7 @@ export class TimelineManager { trimEnd, startTime, duration, + rippleEnabled, }); if (pushHistory) { this.editor.command.execute({ command }); diff --git a/apps/web/src/hooks/timeline/element/use-element-resize.ts b/apps/web/src/hooks/timeline/element/use-element-resize.ts index 0ae17cb6..3f49e798 100644 --- a/apps/web/src/hooks/timeline/element/use-element-resize.ts +++ b/apps/web/src/hooks/timeline/element/use-element-resize.ts @@ -40,7 +40,9 @@ export function useTimelineElementResize({ const activeProject = editor.project.getActive(); const isShiftHeldRef = useShiftKey(); const snappingEnabled = useTimelineStore((state) => state.snappingEnabled); - + const rippleEditingEnabled = useTimelineStore( + (state) => state.rippleEditingEnabled, + ); const [resizing, setResizing] = useState(null); const [currentTrimStart, setCurrentTrimStart] = useState(element.trimStart); @@ -354,6 +356,7 @@ export function useTimelineElementResize({ trimEnd: finalTrimEnd, startTime: startTimeChanged ? finalStartTime : undefined, duration: durationChanged ? finalDuration : undefined, + rippleEnabled: rippleEditingEnabled, }); } @@ -366,6 +369,7 @@ export function useTimelineElementResize({ element.id, onResizeStateChange, onSnapPointChange, + rippleEditingEnabled, ]); useEffect(() => { diff --git a/apps/web/src/lib/commands/timeline/element/update-element-trim.ts b/apps/web/src/lib/commands/timeline/element/update-element-trim.ts index 6afcb9d0..7548638a 100644 --- a/apps/web/src/lib/commands/timeline/element/update-element-trim.ts +++ b/apps/web/src/lib/commands/timeline/element/update-element-trim.ts @@ -2,6 +2,7 @@ import { Command } from "@/lib/commands/base-command"; import type { TimelineTrack } from "@/types/timeline"; import { EditorCore } from "@/core"; import { clampAnimationsToDuration } from "@/lib/animation"; +import { rippleShiftElements } from "@/lib/timeline"; export class UpdateElementTrimCommand extends Command { private savedState: TimelineTrack[] | null = null; @@ -10,6 +11,7 @@ export class UpdateElementTrimCommand extends Command { private readonly trimEnd: number; private readonly startTime: number | undefined; private readonly duration: number | undefined; + private readonly rippleEnabled: boolean; constructor({ elementId, @@ -17,12 +19,14 @@ export class UpdateElementTrimCommand extends Command { trimEnd, startTime, duration, + rippleEnabled = false, }: { elementId: string; trimStart: number; trimEnd: number; startTime?: number; duration?: number; + rippleEnabled?: boolean; }) { super(); this.elementId = elementId; @@ -30,6 +34,7 @@ export class UpdateElementTrimCommand extends Command { this.trimEnd = trimEnd; this.startTime = startTime; this.duration = duration; + this.rippleEnabled = rippleEnabled; } execute(): void { @@ -37,25 +42,52 @@ export class UpdateElementTrimCommand extends Command { this.savedState = editor.timeline.getTracks(); const updatedTracks = this.savedState.map((track) => { - const newElements = track.elements.map((element) => { - if (element.id !== this.elementId) { - return element; - } + const targetElement = track.elements.find( + (element) => element.id === this.elementId, + ); + if (!targetElement) return track; - const nextDuration = this.duration ?? element.duration; - return { - ...element, - trimStart: this.trimStart, - trimEnd: this.trimEnd, - startTime: this.startTime ?? element.startTime, + const nextDuration = this.duration ?? targetElement.duration; + const nextStartTime = this.startTime ?? targetElement.startTime; + + const oldEndTime = targetElement.startTime + targetElement.duration; + const newEndTime = nextStartTime + nextDuration; + const shiftAmount = oldEndTime - newEndTime; + + const updatedElement = { + ...targetElement, + trimStart: this.trimStart, + trimEnd: this.trimEnd, + startTime: nextStartTime, + duration: nextDuration, + animations: clampAnimationsToDuration({ + animations: targetElement.animations, duration: nextDuration, - animations: clampAnimationsToDuration({ - animations: element.animations, - duration: nextDuration, - }), - }; - }); - return { ...track, elements: newElements } as typeof track; + }), + }; + + if (this.rippleEnabled && Math.abs(shiftAmount) > 0) { + const shiftedOthers = rippleShiftElements({ + elements: track.elements.filter((element) => element.id !== this.elementId), + afterTime: oldEndTime, + shiftAmount, + }); + return { + ...track, + elements: track.elements.map((element) => + element.id === this.elementId + ? updatedElement + : (shiftedOthers.find((shifted) => shifted.id === element.id) ?? element) + ), + } as typeof track; + } + + return { + ...track, + elements: track.elements.map((element) => + element.id === this.elementId ? updatedElement : element + ), + } as typeof track; }); editor.timeline.updateTracks(updatedTracks); -- cgit v1.3.1