194 lines
4.1 KiB
TypeScript
194 lines
4.1 KiB
TypeScript
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 | string;
|
|
|
|
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
|
|
);
|
|
}
|