OpenCut/apps/web/src/timeline/animation-targets.ts

236 lines
5.5 KiB
TypeScript

import type {
AnimationBindingKind,
AnimationInterpolation,
AnimationPath,
AnimationValue,
NumericSpec,
} from "@/animation/types";
import {
parseEffectParamPath,
} from "@/animation/effect-param-channel";
import {
parseGraphicParamPath,
} from "@/animation/graphic-param-channel";
import { effectsRegistry, registerDefaultEffects } from "@/effects";
import { getGraphicDefinition } from "@/graphics";
import {
coerceParamValue,
getParamDefaultInterpolation,
getParamNumericRange,
getParamValueKind,
type ParamDefinition,
type ParamValues,
} from "@/params";
import {
getElementParam,
readElementParamValue,
writeElementParamValue,
type ElementParamDefinition,
} from "@/params/registry";
import type { TimelineElement } from "@/timeline";
import { isVisualElement } from "@/timeline/element-utils";
export interface AnimationPathDescriptor {
kind: AnimationBindingKind;
defaultInterpolation: AnimationInterpolation;
numericRanges?: Partial<Record<string, NumericSpec>>;
coerceValue: ({ value }: { value: AnimationValue }) => AnimationValue | null;
getBaseValue: () => AnimationValue | null;
setBaseValue: ({ value }: { value: AnimationValue }) => TimelineElement;
}
// Number/discrete bindings expose a single component named "value"
// (see binding-values.ts). Multi-component kinds (vector2, color) don't carry
// numeric ranges yet — revisit when one does.
function paramNumericRanges({
param,
}: {
param: ParamDefinition;
}): Partial<Record<string, NumericSpec>> | undefined {
const range = getParamNumericRange({ param });
return range ? { value: range } : undefined;
}
function buildParamDescriptor({
param,
baseParams,
setParams,
}: {
param: ParamDefinition;
baseParams: ParamValues;
setParams: (params: ParamValues) => TimelineElement;
}): AnimationPathDescriptor | null {
if (param.keyframable === false) {
return null;
}
return {
kind: getParamValueKind({ param }),
defaultInterpolation: getParamDefaultInterpolation({ param }),
numericRanges: paramNumericRanges({ param }),
coerceValue: ({ value }) => coerceParamValue({ param, value }),
getBaseValue: () => baseParams[param.key] ?? param.default,
setBaseValue: ({ value }) => {
const coercedValue = coerceParamValue({ param, value });
if (coercedValue === null) {
return setParams(baseParams);
}
return setParams({
...baseParams,
[param.key]: coercedValue,
});
},
};
}
function buildElementParamDescriptor({
element,
paramKey,
}: {
element: TimelineElement;
paramKey: string;
}): AnimationPathDescriptor | null {
const param = getElementParam({ element, key: paramKey });
if (!param) {
return null;
}
return buildTimelineElementParamDescriptor({ element, param });
}
function buildTimelineElementParamDescriptor({
element,
param,
}: {
element: TimelineElement;
param: ElementParamDefinition;
}): AnimationPathDescriptor | null {
if (param.keyframable === false) {
return null;
}
return {
kind: getParamValueKind({ param }),
defaultInterpolation: getParamDefaultInterpolation({ param }),
numericRanges: paramNumericRanges({ param }),
coerceValue: ({ value }) => coerceParamValue({ param, value }),
getBaseValue: () => readElementParamValue({ element, param }),
setBaseValue: ({ value }) => {
const coercedValue = coerceParamValue({ param, value });
if (coercedValue === null) {
return element;
}
return writeElementParamValue({ element, param, value: coercedValue });
},
};
}
function buildGraphicParamDescriptor({
element,
paramKey,
}: {
element: TimelineElement;
paramKey: string;
}): AnimationPathDescriptor | null {
if (element.type !== "graphic") {
return null;
}
const definition = getGraphicDefinition({
definitionId: element.definitionId,
});
const param = definition.params.find((candidate) => candidate.key === paramKey);
if (!param) {
return null;
}
return buildParamDescriptor({
param,
baseParams: element.params,
setParams: (params) => ({
...element,
params,
}),
});
}
function buildEffectParamDescriptor({
element,
effectId,
paramKey,
}: {
element: TimelineElement;
effectId: string;
paramKey: string;
}): AnimationPathDescriptor | null {
if (!isVisualElement(element)) {
return null;
}
const effect = element.effects?.find((candidate) => candidate.id === effectId);
if (!effect) {
return null;
}
registerDefaultEffects();
const definition = effectsRegistry.get(effect.type);
const param = definition.params.find((candidate) => candidate.key === paramKey);
if (!param) {
return null;
}
return buildParamDescriptor({
param,
baseParams: effect.params,
setParams: (params) => ({
...element,
effects:
element.effects?.map((candidate) =>
candidate.id !== effectId
? candidate
: {
...candidate,
params,
},
) ?? element.effects,
}),
});
}
export function resolveAnimationTarget({
element,
path,
}: {
element: TimelineElement;
path: AnimationPath;
}): AnimationPathDescriptor | null {
const elementParamTarget = buildElementParamDescriptor({
element,
paramKey: path,
});
if (elementParamTarget) {
return elementParamTarget;
}
const graphicParamTarget = parseGraphicParamPath({ propertyPath: path });
if (graphicParamTarget) {
return buildGraphicParamDescriptor({
element,
paramKey: graphicParamTarget.paramKey,
});
}
const effectParamTarget = parseEffectParamPath({ propertyPath: path });
if (effectParamTarget) {
return buildEffectParamDescriptor({
element,
effectId: effectParamTarget.effectId,
paramKey: effectParamTarget.paramKey,
});
}
return null;
}