diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-04-26 20:44:07 +0200 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-04-26 20:44:07 +0200 |
| commit | 6b1f38a9a31a0eedaa7abea70f232960469351d5 (patch) | |
| tree | 4050d69056c1d9f14075b784535e2b33719f477b /apps/web/src/timeline/animation-targets.ts | |
| parent | 56ca09692a308b22455420d11376ff8d41244c47 (diff) | |
| parent | 0b7597b31fa956c7f3d4838923b545ba5e341422 (diff) | |
| download | opencut-6b1f38a9a31a0eedaa7abea70f232960469351d5.tar.gz | |
refactor: split animation helpers by domain
Diffstat (limited to 'apps/web/src/timeline/animation-targets.ts')
| -rw-r--r-- | apps/web/src/timeline/animation-targets.ts | 214 |
1 files changed, 214 insertions, 0 deletions
diff --git a/apps/web/src/timeline/animation-targets.ts b/apps/web/src/timeline/animation-targets.ts new file mode 100644 index 00000000..f9d53fcc --- /dev/null +++ b/apps/web/src/timeline/animation-targets.ts @@ -0,0 +1,214 @@ +import type { + AnimationBindingKind, + AnimationInterpolation, + AnimationPath, + AnimationValue, + NumericSpec, +} from "@/animation/types"; +import { + coerceAnimationParamValue, + getAnimationParamDefaultInterpolation, + getAnimationParamNumericRange, + getAnimationParamValueKind, +} from "@/animation/animated-params"; +import { + parseEffectParamPath, +} from "@/animation/effect-param-channel"; +import { + parseGraphicParamPath, +} from "@/animation/graphic-param-channel"; +import { effectsRegistry, registerDefaultEffects } from "@/effects"; +import { getGraphicDefinition } from "@/graphics"; +import type { ParamDefinition } from "@/params"; +import type { TimelineElement } from "@/timeline"; +import { isVisualElement } from "@/timeline/element-utils"; +import { isAnimationPropertyPath } from "@/animation/path"; +import { + coerceAnimationValueForProperty, + getAnimationPropertyDefinition, + getElementBaseValueForProperty, + withElementBaseValueForProperty, +} from "./animation-properties"; + +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 = getAnimationParamNumericRange({ param }); + return range ? { value: range } : undefined; +} + +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 { + kind: getAnimationParamValueKind({ param }), + defaultInterpolation: getAnimationParamDefaultInterpolation({ param }), + numericRanges: paramNumericRanges({ param }), + coerceValue: ({ value }) => coerceAnimationParamValue({ param, value }), + getBaseValue: () => element.params[param.key] ?? param.default, + setBaseValue: ({ value }) => { + const coercedValue = coerceAnimationParamValue({ param, value }); + if (coercedValue === null) { + return element; + } + + return { + ...element, + params: { + ...element.params, + [param.key]: coercedValue, + }, + }; + }, + }; +} + +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 { + kind: getAnimationParamValueKind({ param }), + defaultInterpolation: getAnimationParamDefaultInterpolation({ param }), + numericRanges: paramNumericRanges({ param }), + coerceValue: ({ value }) => coerceAnimationParamValue({ param, value }), + getBaseValue: () => effect.params[param.key] ?? param.default, + setBaseValue: ({ value }) => { + const coercedValue = coerceAnimationParamValue({ param, value }); + if (coercedValue === null) { + return element; + } + + return { + ...element, + effects: + element.effects?.map((candidate) => + candidate.id !== effectId + ? candidate + : { + ...candidate, + params: { + ...candidate.params, + [param.key]: coercedValue, + }, + }, + ) ?? element.effects, + }; + }, + }; +} + +export function resolveAnimationTarget({ + element, + path, +}: { + element: TimelineElement; + path: AnimationPath; +}): AnimationPathDescriptor | null { + if (isAnimationPropertyPath(path)) { + const propertyDefinition = getAnimationPropertyDefinition({ + propertyPath: path, + }); + if (!propertyDefinition.supportsElement({ element })) { + return null; + } + + return { + kind: propertyDefinition.kind, + defaultInterpolation: propertyDefinition.defaultInterpolation, + numericRanges: propertyDefinition.numericRanges, + coerceValue: ({ value }) => + coerceAnimationValueForProperty({ + propertyPath: path, + value, + }), + getBaseValue: () => + getElementBaseValueForProperty({ + element, + propertyPath: path, + }), + setBaseValue: ({ value }) => { + const coercedValue = propertyDefinition.coerceValue({ value }); + if (coercedValue === null) { + return element; + } + + return withElementBaseValueForProperty({ + element, + propertyPath: path, + value: coercedValue, + }); + }, + }; + } + + 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; +} |
