From 2ee0a44735d2b930348e9774909638b2c1909738 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 29 Apr 2026 01:28:50 +0200 Subject: refactor: unify element params --- .../animation/__tests__/animated-params.test.ts | 70 ++-- apps/web/src/animation/animated-params.ts | 83 ---- apps/web/src/animation/index.ts | 12 +- apps/web/src/animation/keyframes.ts | 47 --- apps/web/src/animation/property-registry.ts | 425 +++++---------------- apps/web/src/animation/types.ts | 8 +- 6 files changed, 139 insertions(+), 506 deletions(-) delete mode 100644 apps/web/src/animation/animated-params.ts (limited to 'apps/web/src/animation') 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, @@ -30,10 +28,6 @@ import { getScalarSegmentInterpolation, normalizeChannel, } from "./interpolation"; -import { - coerceAnimationValueForProperty, - getAnimationPropertyDefinition, -} from "./property-registry"; import { type MediaTime, roundMediaTime, @@ -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 = ? AnimationPropertyValueMap[TPath] : TPath extends GraphicParamPath | EffectParamPath ? DynamicAnimationPathValue - : never; + : DynamicAnimationPathValue; export type AnimationNumericPropertyPath = { [K in AnimationPropertyPath]: AnimationValueForPath extends number ? K : never; }[AnimationPropertyPath]; -- cgit v1.3.1