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>; 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> | 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; }