summaryrefslogtreecommitdiff
path: root/apps/web/src/timeline/animation-targets.ts
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-04-26 20:44:07 +0200
committerMaze Winther <mazewinther@gmail.com>2026-04-26 20:44:07 +0200
commit6b1f38a9a31a0eedaa7abea70f232960469351d5 (patch)
tree4050d69056c1d9f14075b784535e2b33719f477b /apps/web/src/timeline/animation-targets.ts
parent56ca09692a308b22455420d11376ff8d41244c47 (diff)
parent0b7597b31fa956c7f3d4838923b545ba5e341422 (diff)
downloadopencut-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.ts214
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;
+}