chore: cleanup

This commit is contained in:
Maze Winther 2026-04-29 01:59:46 +02:00
parent 2ee0a44735
commit b8d08df244
10 changed files with 41 additions and 272 deletions

View File

@ -1,200 +0,0 @@
import type {
AnimationBindingKind,
AnimationInterpolation,
AnimationPropertyPath,
AnimationValue,
NumericSpec,
} from "@/animation/types";
import {
coerceParamValue,
getParamDefaultInterpolation,
getParamNumericRange,
getParamValueKind,
type ParamDefinition,
} from "@/params";
import {
getBuiltInElementParams,
getElementParam,
readElementParamValue,
writeElementParamValue,
} from "@/params/registry";
import type { ElementType, TimelineElement } from "@/timeline";
export interface AnimationPropertyDefinition {
kind: AnimationBindingKind;
defaultInterpolation: AnimationInterpolation;
numericRanges?: Partial<Record<string, NumericSpec>>;
supportsElement: ({ element }: { element: TimelineElement }) => boolean;
getValue: ({ element }: { element: TimelineElement }) => AnimationValue | null;
coerceValue: ({ value }: { value: AnimationValue }) => AnimationValue | null;
applyValue: ({
element,
value,
}: {
element: TimelineElement;
value: AnimationValue;
}) => TimelineElement;
}
function getFallbackParam({
propertyPath,
}: {
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;
}
}
return null;
}
function buildDefinition({
propertyPath,
element,
}: {
propertyPath: AnimationPropertyPath;
element?: TimelineElement;
}): AnimationPropertyDefinition | null {
const param = element
? getElementParam({ element, key: propertyPath })
: getFallbackParam({ propertyPath });
if (!param || param.keyframable === false) {
return null;
}
const range = getParamNumericRange({ param });
return {
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 coercedValue = coerceParamValue({
param: targetParam,
value,
});
if (coercedValue === null) {
return candidate;
}
return writeElementParamValue({
element: candidate,
param: targetParam,
value: coercedValue,
});
},
};
}
export function isAnimationPropertyPath(
propertyPath: string,
): propertyPath is AnimationPropertyPath {
return !propertyPath.startsWith("params.") && !propertyPath.startsWith("effects.");
}
export function getAnimationPropertyDefinition({
propertyPath,
element,
}: {
propertyPath: AnimationPropertyPath;
element?: TimelineElement;
}): AnimationPropertyDefinition {
const definition = buildDefinition({ propertyPath, element });
if (!definition) {
throw new Error(`Unknown animation property for element: ${propertyPath}`);
}
return definition;
}
export function supportsAnimationProperty({
element,
propertyPath,
}: {
element: TimelineElement;
propertyPath: AnimationPropertyPath;
}): boolean {
return getElementParam({ element, key: propertyPath }) !== null;
}
export function getElementBaseValueForProperty({
element,
propertyPath,
}: {
element: TimelineElement;
propertyPath: AnimationPropertyPath;
}): AnimationValue | null {
const param = getElementParam({ element, key: propertyPath });
if (!param) {
return null;
}
return readElementParamValue({ element, param });
}
export function withElementBaseValueForProperty({
element,
propertyPath,
value,
}: {
element: TimelineElement;
propertyPath: AnimationPropertyPath;
value: AnimationValue;
}): TimelineElement {
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 {
return getAnimationPropertyDefinition({ propertyPath, element })
.defaultInterpolation;
}
export function coerceAnimationValueForProperty({
propertyPath,
value,
element,
}: {
propertyPath: AnimationPropertyPath;
value: AnimationValue;
element?: TimelineElement;
}): AnimationValue | null {
return getAnimationPropertyDefinition({ propertyPath, element }).coerceValue({
value,
});
}

View File

@ -3,7 +3,7 @@ import { transformProjectV28ToV29 } from "../transformers/v28-to-v29";
import { asRecord, asRecordArray } from "./helpers";
describe("V28 to V29 Migration", () => {
test("moves built-in element fields into params", () => {
test("copies built-in element fields into params without deleting source fields", () => {
const result = transformProjectV28ToV29({
project: {
id: "project-v28-builtins",
@ -83,10 +83,15 @@ describe("V28 to V29 Migration", () => {
const tracks = asRecord(scene.tracks);
const main = asRecord(tracks.main);
const video = asRecordArray(main.elements)[0];
expect(video.transform).toBeUndefined();
expect(video.opacity).toBeUndefined();
expect(video.volume).toBeUndefined();
expect(video.muted).toBeUndefined();
expect(video.transform).toEqual({
position: { x: 12, y: -4 },
scaleX: 1.25,
scaleY: 0.75,
rotate: 9,
});
expect(video.opacity).toBe(0.5);
expect(video.volume).toBe(-6);
expect(video.muted).toBe(true);
expect(video.params).toEqual({
"transform.positionX": 12,
"transform.positionY": -4,
@ -101,8 +106,13 @@ describe("V28 to V29 Migration", () => {
const overlay = asRecordArray(tracks.overlay)[0];
const text = asRecordArray(asRecord(overlay).elements)[0];
expect(text.content).toBeUndefined();
expect(text.background).toBeUndefined();
expect(text.content).toBe("Hello");
expect(text.background).toEqual({
enabled: true,
color: "#111111",
paddingX: 10,
paddingY: 12,
});
expect(text.params).toMatchObject({
content: "Hello",
fontSize: 20,

View File

@ -114,23 +114,6 @@ function migrateElement({ element }: { element: unknown }): unknown {
}
nextElement.params = params;
delete nextElement.transform;
delete nextElement.opacity;
delete nextElement.blendMode;
delete nextElement.volume;
delete nextElement.muted;
delete nextElement.content;
delete nextElement.fontSize;
delete nextElement.fontFamily;
delete nextElement.color;
delete nextElement.background;
delete nextElement.textAlign;
delete nextElement.fontWeight;
delete nextElement.fontStyle;
delete nextElement.textDecoration;
delete nextElement.letterSpacing;
delete nextElement.lineHeight;
return nextElement;
}

View File

@ -6,7 +6,8 @@ import {
} from "@/text/layout";
import { DEFAULTS } from "@/timeline/defaults";
import { mediaTimeFromSeconds } from "@/wasm";
import type { CreateTextElement, TextBackground } from "@/timeline";
import type { CreateTextElement } from "@/timeline";
import type { TextBackground } from "@/text/background";
import type {
TextAlign,
TextDecoration,

View File

@ -1,4 +1,4 @@
import type { TextBackground } from "@/timeline";
import type { TextBackground } from "@/text/background";
import type {
TextAlign,
TextDecoration,

View File

@ -1,2 +1,12 @@
export const CORNER_RADIUS_MIN = 0;
export const CORNER_RADIUS_MAX = 100;
export interface TextBackground {
enabled: boolean;
color: string;
cornerRadius?: number;
paddingX?: number;
paddingY?: number;
offsetX?: number;
offsetY?: number;
}

View File

@ -1,4 +1,4 @@
import type { TextBackground } from "@/timeline";
import type { TextBackground } from "@/text/background";
import { DEFAULTS } from "@/timeline/defaults";
import type { TextAlign } from "@/text/primitives";

View File

@ -1,6 +1,7 @@
import { CORNER_RADIUS_MIN } from "@/text/background";
import { DEFAULTS } from "@/timeline/defaults";
import type { TextBackground, TextElement } from "@/timeline";
import type { TextElement } from "@/timeline";
import type { TextBackground } from "@/text/background";
import { resolveNumberAtTime } from "@/animation/values";
import {
getTextVisualRect,

View File

@ -23,9 +23,6 @@ import {
} from "@/params";
import {
getElementParam,
readElementParamValue,
writeElementParamValue,
type ElementParamDefinition,
} from "@/params/registry";
import type { TimelineElement } from "@/timeline";
import { isVisualElement } from "@/timeline/element-utils";
@ -96,35 +93,14 @@ function buildElementParamDescriptor({
return null;
}
return buildTimelineElementParamDescriptor({ element, param });
}
function buildTimelineElementParamDescriptor({
element,
param,
}: {
element: TimelineElement;
param: ElementParamDefinition;
}): AnimationPathDescriptor | null {
if (param.keyframable === false) {
return null;
}
return {
kind: getParamValueKind({ param }),
defaultInterpolation: getParamDefaultInterpolation({ param }),
numericRanges: paramNumericRanges({ param }),
coerceValue: ({ value }) => coerceParamValue({ param, value }),
getBaseValue: () => readElementParamValue({ element, param }),
setBaseValue: ({ value }) => {
const coercedValue = coerceParamValue({ param, value });
if (coercedValue === null) {
return element;
}
return writeElementParamValue({ element, param, value: coercedValue });
},
};
return buildParamDescriptor({
param,
baseParams: element.params,
setParams: (params) => ({
...element,
params,
}),
});
}
function buildGraphicParamDescriptor({

View File

@ -117,8 +117,6 @@ interface BaseTimelineElement {
export interface VideoElement extends BaseTimelineElement {
type: "video";
mediaId: string;
volume?: number;
muted?: boolean;
isSourceAudioEnabled?: boolean;
hidden?: boolean;
retime?: RetimeConfig;
@ -134,16 +132,6 @@ export interface ImageElement extends BaseTimelineElement {
masks?: Mask[];
}
export interface TextBackground {
enabled: boolean;
color: string;
cornerRadius?: number;
paddingX?: number;
paddingY?: number;
offsetX?: number;
offsetY?: number;
}
export interface TextElement extends BaseTimelineElement {
type: "text";
hidden?: boolean;