diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-04-29 01:28:50 +0200 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-05-02 03:27:23 +0200 |
| commit | 2ee0a44735d2b930348e9774909638b2c1909738 (patch) | |
| tree | e73e60097338ed411739f3b04808dad86f7bb192 /apps/web/src/animation/property-registry.ts | |
| parent | a9b9cf6bf50f08723896659634f2e6afc17bcd42 (diff) | |
| download | opencut-2ee0a44735d2b930348e9774909638b2c1909738.tar.gz | |
refactor: unify element params
Diffstat (limited to 'apps/web/src/animation/property-registry.ts')
| -rw-r--r-- | apps/web/src/animation/property-registry.ts | 425 |
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, + }); } |
