summaryrefslogtreecommitdiff
path: root/apps/web/src/lib/timeline/update-pipeline.ts
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/lib/timeline/update-pipeline.ts')
-rw-r--r--apps/web/src/lib/timeline/update-pipeline.ts193
1 files changed, 193 insertions, 0 deletions
diff --git a/apps/web/src/lib/timeline/update-pipeline.ts b/apps/web/src/lib/timeline/update-pipeline.ts
new file mode 100644
index 00000000..cef99abc
--- /dev/null
+++ b/apps/web/src/lib/timeline/update-pipeline.ts
@@ -0,0 +1,193 @@
+import { clampAnimationsToDuration } from "@/lib/animation";
+import {
+ clampRetimeRate,
+ getSourceSpanAtClipTime,
+ getTimelineDurationForSourceSpan,
+} from "@/lib/retime";
+import { enforceMainTrackStart } from "@/lib/timeline/placement";
+import type { RetimeConfig, TimelineElement, TimelineTrack } from "@/lib/timeline";
+import { isRetimableElement } from "@/lib/timeline";
+
+type ElementUpdateField = keyof TimelineElement;
+
+export interface ElementUpdateContext {
+ tracks: TimelineTrack[];
+ trackId: string;
+}
+
+interface ElementUpdateRuleResult {
+ element: TimelineElement;
+ changedFields?: ElementUpdateField[];
+}
+
+interface ElementUpdateRuleParams {
+ element: TimelineElement;
+ originalElement: TimelineElement;
+ patch: Partial<TimelineElement>;
+ context: ElementUpdateContext;
+}
+
+interface ElementUpdateRule {
+ triggers: ElementUpdateField[];
+ apply: (params: ElementUpdateRuleParams) => ElementUpdateRuleResult;
+}
+
+const deriveRules: ElementUpdateRule[] = [
+ {
+ triggers: ["retime"],
+ apply: ({ element, originalElement, patch }) => {
+ if (!("retime" in patch) || !isRetimableElement(element)) {
+ return { element };
+ }
+
+ const nextRetime = patch.retime
+ ? {
+ ...patch.retime,
+ rate: clampRetimeRate({ rate: patch.retime.rate }),
+ }
+ : undefined;
+
+ const sourceDuration = getSourceDuration({
+ trimStart: originalElement.trimStart,
+ trimEnd: originalElement.trimEnd,
+ duration: originalElement.duration,
+ sourceDuration: isRetimableElement(originalElement)
+ ? originalElement.sourceDuration
+ : undefined,
+ retime: isRetimableElement(originalElement)
+ ? originalElement.retime
+ : undefined,
+ });
+ const visibleSourceSpan = Math.max(
+ 0,
+ sourceDuration - element.trimStart - element.trimEnd,
+ );
+ const nextDuration = getTimelineDurationForSourceSpan({
+ sourceSpan: visibleSourceSpan,
+ retime: nextRetime,
+ });
+
+ return {
+ element: {
+ ...element,
+ retime: nextRetime,
+ duration: nextDuration,
+ },
+ changedFields: ["retime", "duration"],
+ };
+ },
+ },
+];
+
+const enforceRules: ElementUpdateRule[] = [
+ {
+ triggers: ["duration"],
+ apply: ({ element }) => ({
+ element: {
+ ...element,
+ animations: clampAnimationsToDuration({
+ animations: element.animations,
+ duration: element.duration,
+ }),
+ },
+ }),
+ },
+ {
+ triggers: ["startTime"],
+ apply: ({ element, context }) => ({
+ element: {
+ ...element,
+ startTime: enforceMainTrackStart({
+ tracks: context.tracks,
+ targetTrackId: context.trackId,
+ requestedStartTime: Math.max(0, element.startTime),
+ excludeElementId: element.id,
+ }),
+ },
+ }),
+ },
+];
+
+export function applyElementUpdate({
+ element,
+ patch,
+ context,
+}: {
+ element: TimelineElement;
+ patch: Partial<TimelineElement>;
+ context: ElementUpdateContext;
+}): TimelineElement {
+ let nextElement = { ...element, ...patch } as TimelineElement;
+ const changedFields = new Set(
+ Object.keys(patch) as ElementUpdateField[],
+ );
+
+ for (const rule of deriveRules) {
+ if (!shouldApplyRule({ rule, changedFields })) {
+ continue;
+ }
+
+ const result = rule.apply({
+ element: nextElement,
+ originalElement: element,
+ patch,
+ context,
+ });
+ nextElement = result.element;
+ for (const field of result.changedFields ?? []) {
+ changedFields.add(field);
+ }
+ }
+
+ for (const rule of enforceRules) {
+ if (!shouldApplyRule({ rule, changedFields })) {
+ continue;
+ }
+
+ nextElement = rule.apply({
+ element: nextElement,
+ originalElement: element,
+ patch,
+ context,
+ }).element;
+ }
+
+ return nextElement;
+}
+
+function shouldApplyRule({
+ rule,
+ changedFields,
+}: {
+ rule: ElementUpdateRule;
+ changedFields: Set<ElementUpdateField>;
+}): boolean {
+ return rule.triggers.some((trigger) => changedFields.has(trigger));
+}
+
+function getSourceDuration({
+ trimStart,
+ trimEnd,
+ duration,
+ sourceDuration,
+ retime,
+}: {
+ trimStart: number;
+ trimEnd: number;
+ duration: number;
+ sourceDuration?: number;
+ retime?: RetimeConfig;
+}): number {
+ if (typeof sourceDuration === "number") {
+ return sourceDuration;
+ }
+
+ return (
+ trimStart +
+ getSourceSpanAtClipTime({
+ clipTime: duration,
+ retime,
+ }) +
+ trimEnd
+ );
+}