summaryrefslogtreecommitdiff
path: root/apps/web/src/animation
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-04-29 01:28:50 +0200
committerMaze Winther <mazewinther@gmail.com>2026-05-02 03:27:23 +0200
commit2ee0a44735d2b930348e9774909638b2c1909738 (patch)
treee73e60097338ed411739f3b04808dad86f7bb192 /apps/web/src/animation
parenta9b9cf6bf50f08723896659634f2e6afc17bcd42 (diff)
downloadopencut-2ee0a44735d2b930348e9774909638b2c1909738.tar.gz
refactor: unify element params
Diffstat (limited to 'apps/web/src/animation')
-rw-r--r--apps/web/src/animation/__tests__/animated-params.test.ts70
-rw-r--r--apps/web/src/animation/animated-params.ts83
-rw-r--r--apps/web/src/animation/index.ts12
-rw-r--r--apps/web/src/animation/keyframes.ts47
-rw-r--r--apps/web/src/animation/property-registry.ts425
-rw-r--r--apps/web/src/animation/types.ts8
6 files changed, 139 insertions, 506 deletions
diff --git a/apps/web/src/animation/__tests__/animated-params.test.ts b/apps/web/src/animation/__tests__/animated-params.test.ts
index 7b9d312d..a47b0382 100644
--- a/apps/web/src/animation/__tests__/animated-params.test.ts
+++ b/apps/web/src/animation/__tests__/animated-params.test.ts
@@ -1,15 +1,15 @@
import { describe, expect, test } from "bun:test";
import {
- coerceAnimationParamValue,
- getAnimationParamDefaultInterpolation,
- getAnimationParamNumericRange,
- getAnimationParamValueKind,
-} from "@/animation/animated-params";
+ coerceParamValue,
+ getParamDefaultInterpolation,
+ getParamNumericRange,
+ getParamValueKind,
+} from "@/params";
describe("animated params", () => {
test("snaps and clamps number params", () => {
expect(
- coerceAnimationParamValue({
+ coerceParamValue({
param: {
key: "intensity",
label: "Intensity",
@@ -24,7 +24,7 @@ describe("animated params", () => {
).toBe(0.5);
expect(
- coerceAnimationParamValue({
+ coerceParamValue({
param: {
key: "intensity",
label: "Intensity",
@@ -49,14 +49,14 @@ describe("animated params", () => {
max: 1,
step: 0.25,
};
- expect(coerceAnimationParamValue({ param, value: Number.NaN })).toBeNull();
- expect(coerceAnimationParamValue({ param, value: "0.5" })).toBeNull();
- expect(coerceAnimationParamValue({ param, value: true })).toBeNull();
+ expect(coerceParamValue({ param, value: Number.NaN })).toBeNull();
+ expect(coerceParamValue({ param, value: "0.5" })).toBeNull();
+ expect(coerceParamValue({ param, value: true })).toBeNull();
});
test("passthrough with step <= 0 guard", () => {
expect(
- coerceAnimationParamValue({
+ coerceParamValue({
param: {
key: "x",
label: "X",
@@ -81,13 +81,13 @@ describe("animated params", () => {
{ value: "multiply", label: "Multiply" },
],
};
- expect(coerceAnimationParamValue({ param, value: "normal" })).toBe("normal");
- expect(coerceAnimationParamValue({ param, value: "multiply" })).toBe("multiply");
+ expect(coerceParamValue({ param, value: "normal" })).toBe("normal");
+ expect(coerceParamValue({ param, value: "multiply" })).toBe("multiply");
});
test("rejects select values outside the allowed options", () => {
expect(
- coerceAnimationParamValue({
+ coerceParamValue({
param: {
key: "blend",
label: "Blend",
@@ -111,9 +111,9 @@ describe("animated params", () => {
default: "normal",
options: [{ value: "normal", label: "Normal" }],
};
- expect(coerceAnimationParamValue({ param, value: 42 })).toBeNull();
- expect(coerceAnimationParamValue({ param, value: null })).toBeNull();
- expect(coerceAnimationParamValue({ param, value: undefined })).toBeNull();
+ expect(coerceParamValue({ param, value: 42 })).toBeNull();
+ expect(coerceParamValue({ param, value: null })).toBeNull();
+ expect(coerceParamValue({ param, value: undefined })).toBeNull();
});
test("boolean params accept booleans and reject other types", () => {
@@ -123,10 +123,10 @@ describe("animated params", () => {
type: "boolean" as const,
default: true,
};
- expect(coerceAnimationParamValue({ param, value: true })).toBe(true);
- expect(coerceAnimationParamValue({ param, value: false })).toBe(false);
- expect(coerceAnimationParamValue({ param, value: 1 })).toBeNull();
- expect(coerceAnimationParamValue({ param, value: "true" })).toBeNull();
+ expect(coerceParamValue({ param, value: true })).toBe(true);
+ expect(coerceParamValue({ param, value: false })).toBe(false);
+ expect(coerceParamValue({ param, value: 1 })).toBeNull();
+ expect(coerceParamValue({ param, value: "true" })).toBeNull();
});
test("color params accept strings and reject other types", () => {
@@ -136,14 +136,14 @@ describe("animated params", () => {
type: "color" as const,
default: "#ffffff",
};
- expect(coerceAnimationParamValue({ param, value: "#ff0000" })).toBe("#ff0000");
- expect(coerceAnimationParamValue({ param, value: 0xff0000 })).toBeNull();
- expect(coerceAnimationParamValue({ param, value: null })).toBeNull();
+ expect(coerceParamValue({ param, value: "#ff0000" })).toBe("#ff0000");
+ expect(coerceParamValue({ param, value: 0xff0000 })).toBeNull();
+ expect(coerceParamValue({ param, value: null })).toBeNull();
});
test("getAnimationParamValueKind maps param type to binding kind", () => {
expect(
- getAnimationParamValueKind({
+ getParamValueKind({
param: {
key: "n",
label: "N",
@@ -155,17 +155,17 @@ describe("animated params", () => {
}),
).toBe("number");
expect(
- getAnimationParamValueKind({
+ getParamValueKind({
param: { key: "c", label: "C", type: "color", default: "#fff" },
}),
).toBe("color");
expect(
- getAnimationParamValueKind({
+ getParamValueKind({
param: { key: "b", label: "B", type: "boolean", default: false },
}),
).toBe("discrete");
expect(
- getAnimationParamValueKind({
+ getParamValueKind({
param: {
key: "s",
label: "S",
@@ -179,7 +179,7 @@ describe("animated params", () => {
test("getAnimationParamDefaultInterpolation is linear for continuous, hold for discrete", () => {
expect(
- getAnimationParamDefaultInterpolation({
+ getParamDefaultInterpolation({
param: {
key: "n",
label: "N",
@@ -191,17 +191,17 @@ describe("animated params", () => {
}),
).toBe("linear");
expect(
- getAnimationParamDefaultInterpolation({
+ getParamDefaultInterpolation({
param: { key: "c", label: "C", type: "color", default: "#fff" },
}),
).toBe("linear");
expect(
- getAnimationParamDefaultInterpolation({
+ getParamDefaultInterpolation({
param: { key: "b", label: "B", type: "boolean", default: false },
}),
).toBe("hold");
expect(
- getAnimationParamDefaultInterpolation({
+ getParamDefaultInterpolation({
param: {
key: "s",
label: "S",
@@ -215,7 +215,7 @@ describe("animated params", () => {
test("getAnimationParamNumericRange returns spec for number params, undefined otherwise", () => {
expect(
- getAnimationParamNumericRange({
+ getParamNumericRange({
param: {
key: "intensity",
label: "Intensity",
@@ -228,12 +228,12 @@ describe("animated params", () => {
}),
).toEqual({ min: 0, max: 1, step: 0.1 });
expect(
- getAnimationParamNumericRange({
+ getParamNumericRange({
param: { key: "c", label: "C", type: "color", default: "#fff" },
}),
).toBeUndefined();
expect(
- getAnimationParamNumericRange({
+ getParamNumericRange({
param: { key: "b", label: "B", type: "boolean", default: false },
}),
).toBeUndefined();
diff --git a/apps/web/src/animation/animated-params.ts b/apps/web/src/animation/animated-params.ts
deleted file mode 100644
index bc27b15d..00000000
--- a/apps/web/src/animation/animated-params.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-import { snapToStep } from "@/utils/math";
-import type { ParamDefinition } from "@/params";
-import type { DynamicAnimationPathValue, NumericSpec } from "./types";
-
-export function getAnimationParamValueKind({
- param,
-}: {
- param: ParamDefinition;
-}): "number" | "color" | "discrete" {
- if (param.type === "number") {
- return "number";
- }
-
- if (param.type === "color") {
- return "color";
- }
-
- return "discrete";
-}
-
-export function getAnimationParamDefaultInterpolation({
- param,
-}: {
- param: ParamDefinition;
-}): "linear" | "hold" {
- return param.type === "number" || param.type === "color" ? "linear" : "hold";
-}
-
-export function getAnimationParamNumericRange({
- param,
-}: {
- param: ParamDefinition;
-}): NumericSpec | undefined {
- if (param.type !== "number") {
- return undefined;
- }
-
- return {
- min: param.min,
- max: param.max,
- step: param.step,
- };
-}
-
-/**
- * `coerceAnimationParamValue` accepts `unknown` rather than a narrow
- * `DynamicAnimationPathValue` because it doubles as a runtime gate for
- * untrusted inputs (persisted state, paste payloads, programmatic updates).
- * The caller does not need to pre-validate; null means "this value cannot live
- * on this param".
- */
-export function coerceAnimationParamValue({
- param,
- value,
-}: {
- param: ParamDefinition;
- value: unknown;
-}): DynamicAnimationPathValue | null {
- if (param.type === "number") {
- if (typeof value !== "number" || Number.isNaN(value)) {
- return null;
- }
-
- const steppedValue = snapToStep({ value, step: param.step });
- const minValue = param.min;
- const maxValue = param.max ?? Number.POSITIVE_INFINITY;
- return Math.min(maxValue, Math.max(minValue, steppedValue));
- }
-
- if (param.type === "color") {
- return typeof value === "string" ? value : null;
- }
-
- if (param.type === "boolean") {
- return typeof value === "boolean" ? value : null;
- }
-
- if (typeof value !== "string") {
- return null;
- }
-
- return param.options.some((option) => option.value === value) ? value : null;
-}
diff --git a/apps/web/src/animation/index.ts b/apps/web/src/animation/index.ts
index 33e637c3..bed3b796 100644
--- a/apps/web/src/animation/index.ts
+++ b/apps/web/src/animation/index.ts
@@ -16,7 +16,6 @@ export {
setChannel,
splitAnimationsAtTime,
updateScalarKeyframeCurve,
- upsertElementKeyframe,
upsertPathKeyframe,
} from "./keyframes";
@@ -75,13 +74,4 @@ export {
isAnimationPropertyPath,
} from "./path";
-export {
- coerceAnimationValueForProperty,
- getAnimationPropertyDefinition,
- getDefaultInterpolationForProperty,
- getElementBaseValueForProperty,
- supportsAnimationProperty,
- type AnimationPropertyDefinition,
- type NumericSpec,
- withElementBaseValueForProperty,
-} from "./property-registry";
+export type { NumericSpec } from "./types";
diff --git a/apps/web/src/animation/keyframes.ts b/apps/web/src/animation/keyframes.ts
index 70f9b5db..d14329c1 100644
--- a/apps/web/src/animation/keyframes.ts
+++ b/apps/web/src/animation/keyframes.ts
@@ -4,7 +4,6 @@ import type {
AnimationChannel,
AnimationInterpolation,
AnimationPath,
- AnimationPropertyPath,
AnimationValue,
DiscreteAnimationChannel,
DiscreteAnimationKey,
@@ -20,7 +19,6 @@ import {
decomposeAnimationValue,
} from "./binding-values";
import {
- getBezierPoint,
getDefaultLeftHandle,
getDefaultRightHandle,
solveBezierProgressForTime,
@@ -31,10 +29,6 @@ import {
normalizeChannel,
} from "./interpolation";
import {
- coerceAnimationValueForProperty,
- getAnimationPropertyDefinition,
-} from "./property-registry";
-import {
type MediaTime,
roundMediaTime,
subMediaTime,
@@ -533,47 +527,6 @@ export function upsertPathKeyframe({
});
}
-export function upsertElementKeyframe({
- animations,
- propertyPath,
- time,
- value,
- interpolation,
- keyframeId,
-}: {
- animations: ElementAnimations | undefined;
- propertyPath: AnimationPropertyPath;
- time: MediaTime;
- value: AnimationValue;
- interpolation?: AnimationInterpolation;
- keyframeId?: string;
-}): ElementAnimations | undefined {
- const coercedValue = coerceAnimationValueForProperty({
- propertyPath,
- value,
- });
- if (coercedValue === null) {
- return animations;
- }
-
- const propertyDefinition = getAnimationPropertyDefinition({ propertyPath });
- return upsertPathKeyframe({
- animations,
- propertyPath,
- time,
- value: coercedValue,
- interpolation,
- keyframeId,
- kind: propertyDefinition.kind,
- defaultInterpolation: propertyDefinition.defaultInterpolation,
- coerceValue: ({ value: nextValue }) =>
- coerceAnimationValueForProperty({
- propertyPath,
- value: nextValue,
- }),
- });
-}
-
export function upsertKeyframe({
channel,
time,
diff --git a/apps/web/src/animation/property-registry.ts b/apps/web/src/animation/property-registry.ts
index bca9d82e..5ba45204 100644
--- a/apps/web/src/animation/property-registry.ts
+++ b/apps/web/src/animation/property-registry.ts
@@ -3,27 +3,22 @@ import type {
AnimationInterpolation,
AnimationPropertyPath,
AnimationValue,
+ NumericSpec,
} from "@/animation/types";
-import { parseColorToLinearRgba } from "./binding-values";
-import type { TimelineElement } from "@/timeline";
-import { MIN_TRANSFORM_SCALE } from "@/animation/transform";
import {
- CORNER_RADIUS_MAX,
- CORNER_RADIUS_MIN,
-} from "@/text/background";
+ coerceParamValue,
+ getParamDefaultInterpolation,
+ getParamNumericRange,
+ getParamValueKind,
+ type ParamDefinition,
+} from "@/params";
import {
- canElementHaveAudio,
- isVisualElement,
-} from "@/timeline/element-utils";
-import { VOLUME_DB_MAX, VOLUME_DB_MIN } from "@/timeline/audio-constants";
-import { DEFAULTS } from "@/timeline/defaults";
-import { snapToStep } from "@/utils/math";
-
-export interface NumericSpec {
- min?: number;
- max?: number;
- step?: number;
-}
+ getBuiltInElementParams,
+ getElementParam,
+ readElementParamValue,
+ writeElementParamValue,
+} from "@/params/registry";
+import type { ElementType, TimelineElement } from "@/timeline";
export interface AnimationPropertyDefinition {
kind: AnimationBindingKind;
@@ -32,10 +27,6 @@ export interface AnimationPropertyDefinition {
supportsElement: ({ element }: { element: TimelineElement }) => boolean;
getValue: ({ element }: { element: TimelineElement }) => AnimationValue | null;
coerceValue: ({ value }: { value: AnimationValue }) => AnimationValue | null;
- // Apply `value` to `element` for this property. Coerces the value through
- // `coerceValue` and verifies element support; returns `element` unchanged
- // if either fails. Cannot be bypassed — there is no kind-narrow `setValue`
- // on the public surface, so callers can't apply an unvalidated value.
applyValue: ({
element,
value,
@@ -45,323 +36,98 @@ export interface AnimationPropertyDefinition {
}) => TimelineElement;
}
-function applyNumericSpec({
- value,
- numericRange,
+function getFallbackParam({
+ propertyPath,
}: {
- value: number;
- numericRange: NumericSpec | undefined;
-}): number {
- if (!numericRange) {
- return value;
+ propertyPath: AnimationPropertyPath;
+}): ParamDefinition | null {
+ const elementTypes: ElementType[] = [
+ "video",
+ "image",
+ "text",
+ "sticker",
+ "graphic",
+ "audio",
+ ];
+ for (const type of elementTypes) {
+ const param =
+ getBuiltInElementParams({ type }).find(
+ (candidate) => candidate.key === propertyPath,
+ ) ?? null;
+ if (param) {
+ return param;
+ }
}
-
- const steppedValue =
- numericRange.step != null
- ? snapToStep({ value, step: numericRange.step })
- : value;
- const minValue = numericRange.min ?? Number.NEGATIVE_INFINITY;
- const maxValue = numericRange.max ?? Number.POSITIVE_INFINITY;
- return Math.min(maxValue, Math.max(minValue, steppedValue));
+ return null;
}
-function coerceNumberValue({
- value,
- numericRange,
+function buildDefinition({
+ propertyPath,
+ element,
}: {
- value: AnimationValue;
- numericRange?: NumericSpec;
-}): number | null {
- if (typeof value !== "number" || Number.isNaN(value)) {
+ propertyPath: AnimationPropertyPath;
+ element?: TimelineElement;
+}): AnimationPropertyDefinition | null {
+ const param = element
+ ? getElementParam({ element, key: propertyPath })
+ : getFallbackParam({ propertyPath });
+ if (!param || param.keyframable === false) {
return null;
}
- return applyNumericSpec({ value, numericRange });
-}
-
-function coerceColorValue({
- value,
-}: {
- value: AnimationValue;
-}): string | null {
- return typeof value === "string" && parseColorToLinearRgba({ color: value })
- ? value
- : null;
-}
-
-function createNumberPropertyDefinition({
- numericRange,
- supportsElement,
- getValue,
- setValue,
-}: {
- numericRange?: NumericSpec;
- supportsElement: AnimationPropertyDefinition["supportsElement"];
- getValue: AnimationPropertyDefinition["getValue"];
- setValue: ({
- element,
- value,
- }: {
- element: TimelineElement;
- value: number;
- }) => TimelineElement;
-}): AnimationPropertyDefinition {
- return {
- kind: "number",
- defaultInterpolation: "linear",
- numericRanges: numericRange ? { value: numericRange } : undefined,
- supportsElement,
- getValue,
- coerceValue: ({ value }) => coerceNumberValue({ value, numericRange }),
- applyValue: ({ element, value }) => {
- if (!supportsElement({ element })) {
- return element;
- }
- const coerced = coerceNumberValue({ value, numericRange });
- if (coerced === null) {
- return element;
- }
- return setValue({ element, value: coerced });
- },
- };
-}
+ const range = getParamNumericRange({ param });
-function createColorPropertyDefinition({
- supportsElement,
- getValue,
- setValue,
-}: {
- supportsElement: AnimationPropertyDefinition["supportsElement"];
- getValue: AnimationPropertyDefinition["getValue"];
- setValue: ({
- element,
- value,
- }: {
- element: TimelineElement;
- value: string;
- }) => TimelineElement;
-}): AnimationPropertyDefinition {
return {
- kind: "color",
- defaultInterpolation: "linear",
- supportsElement,
- getValue,
- coerceValue: ({ value }) => coerceColorValue({ value }),
- applyValue: ({ element, value }) => {
- if (!supportsElement({ element })) {
- return element;
+ kind: getParamValueKind({ param }),
+ defaultInterpolation: getParamDefaultInterpolation({ param }),
+ numericRanges: range ? { value: range } : undefined,
+ supportsElement: ({ element: candidate }) =>
+ getElementParam({ element: candidate, key: propertyPath }) !== null,
+ getValue: ({ element: candidate }) =>
+ readElementParamValue({ element: candidate, param }),
+ coerceValue: ({ value }) => coerceParamValue({ param, value }),
+ applyValue: ({ element: candidate, value }) => {
+ const targetParam = getElementParam({
+ element: candidate,
+ key: propertyPath,
+ });
+ if (!targetParam) {
+ return candidate;
}
- const coerced = coerceColorValue({ value });
- if (coerced === null) {
- return element;
+ const coercedValue = coerceParamValue({
+ param: targetParam,
+ value,
+ });
+ if (coercedValue === null) {
+ return candidate;
}
- return setValue({ element, value: coerced });
+ return writeElementParamValue({
+ element: candidate,
+ param: targetParam,
+ value: coercedValue,
+ });
},
};
}
-const ANIMATION_PROPERTY_REGISTRY: Record<
- AnimationPropertyPath,
- AnimationPropertyDefinition
-> = {
- "transform.positionX": createNumberPropertyDefinition({
- numericRange: { step: 1 },
- supportsElement: ({ element }) => isVisualElement(element),
- getValue: ({ element }) =>
- isVisualElement(element) ? element.transform.position.x : null,
- setValue: ({ element, value }) =>
- isVisualElement(element)
- ? {
- ...element,
- transform: {
- ...element.transform,
- position: { ...element.transform.position, x: value },
- },
- }
- : element,
- }),
- "transform.positionY": createNumberPropertyDefinition({
- numericRange: { step: 1 },
- supportsElement: ({ element }) => isVisualElement(element),
- getValue: ({ element }) =>
- isVisualElement(element) ? element.transform.position.y : null,
- setValue: ({ element, value }) =>
- isVisualElement(element)
- ? {
- ...element,
- transform: {
- ...element.transform,
- position: { ...element.transform.position, y: value },
- },
- }
- : element,
- }),
- "transform.scaleX": createNumberPropertyDefinition({
- numericRange: { min: MIN_TRANSFORM_SCALE, step: 0.01 },
- supportsElement: ({ element }) => isVisualElement(element),
- getValue: ({ element }) =>
- isVisualElement(element) ? element.transform.scaleX : null,
- setValue: ({ element, value }) =>
- isVisualElement(element)
- ? {
- ...element,
- transform: { ...element.transform, scaleX: value },
- }
- : element,
- }),
- "transform.scaleY": createNumberPropertyDefinition({
- numericRange: { min: MIN_TRANSFORM_SCALE, step: 0.01 },
- supportsElement: ({ element }) => isVisualElement(element),
- getValue: ({ element }) =>
- isVisualElement(element) ? element.transform.scaleY : null,
- setValue: ({ element, value }) =>
- isVisualElement(element)
- ? {
- ...element,
- transform: { ...element.transform, scaleY: value },
- }
- : element,
- }),
- "transform.rotate": createNumberPropertyDefinition({
- numericRange: { min: -360, max: 360, step: 1 },
- supportsElement: ({ element }) => isVisualElement(element),
- getValue: ({ element }) =>
- isVisualElement(element) ? element.transform.rotate : null,
- setValue: ({ element, value }) =>
- isVisualElement(element)
- ? {
- ...element,
- transform: { ...element.transform, rotate: value },
- }
- : element,
- }),
- opacity: createNumberPropertyDefinition({
- numericRange: { min: 0, max: 1, step: 0.01 },
- supportsElement: ({ element }) => isVisualElement(element),
- getValue: ({ element }) =>
- isVisualElement(element) ? element.opacity : null,
- setValue: ({ element, value }) =>
- isVisualElement(element) ? { ...element, opacity: value } : element,
- }),
- volume: createNumberPropertyDefinition({
- numericRange: { min: VOLUME_DB_MIN, max: VOLUME_DB_MAX, step: 0.01 },
- supportsElement: ({ element }) => canElementHaveAudio(element),
- getValue: ({ element }) =>
- canElementHaveAudio(element) ? element.volume ?? 0 : null,
- setValue: ({ element, value }) =>
- canElementHaveAudio(element) ? { ...element, volume: value } : element,
- }),
- color: createColorPropertyDefinition({
- supportsElement: ({ element }) => element.type === "text",
- getValue: ({ element }) => (element.type === "text" ? element.color : null),
- setValue: ({ element, value }) =>
- element.type === "text" ? { ...element, color: value } : element,
- }),
- "background.color": createColorPropertyDefinition({
- supportsElement: ({ element }) => element.type === "text",
- getValue: ({ element }) =>
- element.type === "text" ? element.background.color : null,
- setValue: ({ element, value }) =>
- element.type === "text"
- ? {
- ...element,
- background: { ...element.background, color: value },
- }
- : element,
- }),
- "background.paddingX": createNumberPropertyDefinition({
- numericRange: { min: 0, step: 1 },
- supportsElement: ({ element }) => element.type === "text",
- getValue: ({ element }) =>
- element.type === "text"
- ? (element.background.paddingX ?? DEFAULTS.text.background.paddingX)
- : null,
- setValue: ({ element, value }) =>
- element.type === "text"
- ? {
- ...element,
- background: { ...element.background, paddingX: value },
- }
- : element,
- }),
- "background.paddingY": createNumberPropertyDefinition({
- numericRange: { min: 0, step: 1 },
- supportsElement: ({ element }) => element.type === "text",
- getValue: ({ element }) =>
- element.type === "text"
- ? (element.background.paddingY ?? DEFAULTS.text.background.paddingY)
- : null,
- setValue: ({ element, value }) =>
- element.type === "text"
- ? {
- ...element,
- background: { ...element.background, paddingY: value },
- }
- : element,
- }),
- "background.offsetX": createNumberPropertyDefinition({
- numericRange: { step: 1 },
- supportsElement: ({ element }) => element.type === "text",
- getValue: ({ element }) =>
- element.type === "text"
- ? (element.background.offsetX ?? DEFAULTS.text.background.offsetX)
- : null,
- setValue: ({ element, value }) =>
- element.type === "text"
- ? {
- ...element,
- background: { ...element.background, offsetX: value },
- }
- : element,
- }),
- "background.offsetY": createNumberPropertyDefinition({
- numericRange: { step: 1 },
- supportsElement: ({ element }) => element.type === "text",
- getValue: ({ element }) =>
- element.type === "text"
- ? (element.background.offsetY ?? DEFAULTS.text.background.offsetY)
- : null,
- setValue: ({ element, value }) =>
- element.type === "text"
- ? {
- ...element,
- background: { ...element.background, offsetY: value },
- }
- : element,
- }),
- "background.cornerRadius": createNumberPropertyDefinition({
- numericRange: {
- min: CORNER_RADIUS_MIN,
- max: CORNER_RADIUS_MAX,
- step: 1,
- },
- supportsElement: ({ element }) => element.type === "text",
- getValue: ({ element }) =>
- element.type === "text"
- ? (element.background.cornerRadius ?? CORNER_RADIUS_MIN)
- : null,
- setValue: ({ element, value }) =>
- element.type === "text"
- ? {
- ...element,
- background: { ...element.background, cornerRadius: value },
- }
- : element,
- }),
-};
-
export function isAnimationPropertyPath(
propertyPath: string,
): propertyPath is AnimationPropertyPath {
- return Object.hasOwn(ANIMATION_PROPERTY_REGISTRY, propertyPath);
+ return !propertyPath.startsWith("params.") && !propertyPath.startsWith("effects.");
}
export function getAnimationPropertyDefinition({
propertyPath,
+ element,
}: {
propertyPath: AnimationPropertyPath;
+ element?: TimelineElement;
}): AnimationPropertyDefinition {
- return ANIMATION_PROPERTY_REGISTRY[propertyPath];
+ const definition = buildDefinition({ propertyPath, element });
+ if (!definition) {
+ throw new Error(`Unknown animation property for element: ${propertyPath}`);
+ }
+ return definition;
}
export function supportsAnimationProperty({
@@ -371,8 +137,7 @@ export function supportsAnimationProperty({
element: TimelineElement;
propertyPath: AnimationPropertyPath;
}): boolean {
- const propertyDefinition = getAnimationPropertyDefinition({ propertyPath });
- return propertyDefinition.supportsElement({ element });
+ return getElementParam({ element, key: propertyPath }) !== null;
}
export function getElementBaseValueForProperty({
@@ -382,11 +147,11 @@ export function getElementBaseValueForProperty({
element: TimelineElement;
propertyPath: AnimationPropertyPath;
}): AnimationValue | null {
- const definition = getAnimationPropertyDefinition({ propertyPath });
- if (!definition.supportsElement({ element })) {
+ const param = getElementParam({ element, key: propertyPath });
+ if (!param) {
return null;
}
- return definition.getValue({ element });
+ return readElementParamValue({ element, param });
}
export function withElementBaseValueForProperty({
@@ -398,26 +163,38 @@ export function withElementBaseValueForProperty({
propertyPath: AnimationPropertyPath;
value: AnimationValue;
}): TimelineElement {
- const definition = getAnimationPropertyDefinition({ propertyPath });
- return definition.applyValue({ element, value });
+ const param = getElementParam({ element, key: propertyPath });
+ if (!param) {
+ return element;
+ }
+ const coercedValue = coerceParamValue({ param, value });
+ if (coercedValue === null) {
+ return element;
+ }
+ return writeElementParamValue({ element, param, value: coercedValue });
}
export function getDefaultInterpolationForProperty({
propertyPath,
+ element,
}: {
propertyPath: AnimationPropertyPath;
+ element?: TimelineElement;
}): AnimationInterpolation {
- const propertyDefinition = getAnimationPropertyDefinition({ propertyPath });
- return propertyDefinition.defaultInterpolation;
+ return getAnimationPropertyDefinition({ propertyPath, element })
+ .defaultInterpolation;
}
export function coerceAnimationValueForProperty({
propertyPath,
value,
+ element,
}: {
propertyPath: AnimationPropertyPath;
value: AnimationValue;
+ element?: TimelineElement;
}): AnimationValue | null {
- const propertyDefinition = getAnimationPropertyDefinition({ propertyPath });
- return propertyDefinition.coerceValue({ value });
+ return getAnimationPropertyDefinition({ propertyPath, element }).coerceValue({
+ value,
+ });
}
diff --git a/apps/web/src/animation/types.ts b/apps/web/src/animation/types.ts
index cfecfada..08fced74 100644
--- a/apps/web/src/animation/types.ts
+++ b/apps/web/src/animation/types.ts
@@ -1,4 +1,3 @@
-import type { ParamValues } from "@/params";
import type { MediaTime } from "@/wasm";
export const ANIMATION_PROPERTY_PATHS = [
@@ -21,10 +20,7 @@ export const ANIMATION_PROPERTY_PATHS = [
export type AnimationPropertyPath = (typeof ANIMATION_PROPERTY_PATHS)[number];
export type GraphicParamPath = `params.${string}`;
export type EffectParamPath = `effects.${string}.params.${string}`;
-export type AnimationPath =
- | AnimationPropertyPath
- | GraphicParamPath
- | EffectParamPath;
+export type AnimationPath = string;
export const ANIMATION_PROPERTY_GROUPS = {
"transform.scale": ["transform.scaleX", "transform.scaleY"],
@@ -63,7 +59,7 @@ export type AnimationValueForPath<TPath extends AnimationPath> =
? AnimationPropertyValueMap[TPath]
: TPath extends GraphicParamPath | EffectParamPath
? DynamicAnimationPathValue
- : never;
+ : DynamicAnimationPathValue;
export type AnimationNumericPropertyPath = {
[K in AnimationPropertyPath]: AnimationValueForPath<K> extends number ? K : never;
}[AnimationPropertyPath];