summaryrefslogtreecommitdiff
path: root/apps/web/src/animation/property-registry.ts
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/animation/property-registry.ts')
-rw-r--r--apps/web/src/animation/property-registry.ts425
1 files changed, 101 insertions, 324 deletions
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,
+ });
}