refactor: new parameters modulerefactor: unify element params

This commit is contained in:
Maze Winther
2026-04-29 01:28:50 +02:00
parent d7692b4c79
commit 222ef055fc
53 changed files with 2021 additions and 1362 deletions
@@ -1,11 +1,14 @@
import { BaseNode } from "./base-node";
import type { TextElement } from "@/timeline";
import type { EffectPass } from "@/effects/types";
import type { Transform } from "@/rendering";
import type { BlendMode, Transform } from "@/rendering";
import { drawMeasuredTextLayout } from "@/text/primitives";
import type { MeasuredTextElement } from "@/text/measure-element";
export type TextNodeParams = TextElement & {
transform: Transform;
opacity: number;
blendMode?: BlendMode;
canvasCenter: { x: number; y: number };
canvasHeight: number;
textBaseline?: CanvasTextBaseline;
+7 -2
View File
@@ -13,6 +13,7 @@ import {
resolveGraphicElementParamsAtTime,
} from "@/graphics";
import {
buildTextBackgroundFromElement,
getTextMeasurementContext,
measureTextElement,
} from "@/text/measure-element";
@@ -330,6 +331,7 @@ function resolveTextNode({
elementStartTime: node.params.startTime,
elementDuration: node.params.duration,
});
const background = buildTextBackgroundFromElement({ element: node.params });
return {
transform: resolveTransformAtTime({
@@ -343,13 +345,16 @@ function resolveTextNode({
localTime,
}),
textColor: resolveColorAtTime({
baseColor: node.params.color,
baseColor:
typeof node.params.params.color === "string"
? node.params.params.color
: "#ffffff",
animations: node.params.animations,
propertyPath: "color",
localTime,
}),
backgroundColor: resolveColorAtTime({
baseColor: node.params.background.color,
baseColor: background.color,
animations: node.params.animations,
propertyPath: "background.color",
localTime,
+20 -12
View File
@@ -12,6 +12,11 @@ import { EffectLayerNode } from "./nodes/effect-layer-node";
import type { AnyBaseNode } from "./nodes/base-node";
import type { TBackground, TCanvasSize } from "@/project/types";
import { DEFAULT_BACKGROUND_BLUR_INTENSITY } from "@/background/blur";
import {
buildTransformFromParams,
readBlendModeFromParams,
readOpacityFromParams,
} from "@/rendering";
const PREVIEW_MAX_IMAGE_SIZE = 2048;
@@ -71,10 +76,10 @@ function buildTrackNodes({
trimStart: element.trimStart,
trimEnd: element.trimEnd,
retime: element.retime,
transform: element.transform,
transform: buildTransformFromParams({ params: element.params }),
animations: element.animations,
opacity: element.opacity,
blendMode: element.blendMode,
opacity: readOpacityFromParams({ params: element.params }),
blendMode: readBlendModeFromParams({ params: element.params }),
effects: element.effects ?? [],
masks: element.masks ?? [],
}),
@@ -88,10 +93,10 @@ function buildTrackNodes({
timeOffset: element.startTime,
trimStart: element.trimStart,
trimEnd: element.trimEnd,
transform: element.transform,
transform: buildTransformFromParams({ params: element.params }),
animations: element.animations,
opacity: element.opacity,
blendMode: element.blendMode,
opacity: readOpacityFromParams({ params: element.params }),
blendMode: readBlendModeFromParams({ params: element.params }),
effects: element.effects ?? [],
masks: element.masks ?? [],
...(isPreview && {
@@ -106,6 +111,9 @@ function buildTrackNodes({
nodes.push(
new TextNode({
...element,
transform: buildTransformFromParams({ params: element.params }),
opacity: readOpacityFromParams({ params: element.params }),
blendMode: readBlendModeFromParams({ params: element.params }),
canvasCenter: { x: canvasSize.width / 2, y: canvasSize.height / 2 },
canvasHeight: canvasSize.height,
textBaseline: "middle",
@@ -124,10 +132,10 @@ function buildTrackNodes({
timeOffset: element.startTime,
trimStart: element.trimStart,
trimEnd: element.trimEnd,
transform: element.transform,
transform: buildTransformFromParams({ params: element.params }),
animations: element.animations,
opacity: element.opacity,
blendMode: element.blendMode,
opacity: readOpacityFromParams({ params: element.params }),
blendMode: readBlendModeFromParams({ params: element.params }),
effects: element.effects ?? [],
}),
);
@@ -142,10 +150,10 @@ function buildTrackNodes({
timeOffset: element.startTime,
trimStart: element.trimStart,
trimEnd: element.trimEnd,
transform: element.transform,
transform: buildTransformFromParams({ params: element.params }),
animations: element.animations,
opacity: element.opacity,
blendMode: element.blendMode,
opacity: readOpacityFromParams({ params: element.params }),
blendMode: readBlendModeFromParams({ params: element.params }),
effects: element.effects ?? [],
masks: element.masks ?? [],
}),
@@ -0,0 +1,125 @@
import { describe, expect, test } from "bun:test";
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", () => {
const result = transformProjectV28ToV29({
project: {
id: "project-v28-builtins",
version: 28,
scenes: [
{
id: "scene-1",
tracks: {
main: {
id: "main",
type: "video",
elements: [
{
id: "video-1",
type: "video",
name: "Video",
transform: {
position: { x: 12, y: -4 },
scaleX: 1.25,
scaleY: 0.75,
rotate: 9,
},
opacity: 0.5,
blendMode: "multiply",
volume: -6,
muted: true,
},
],
},
overlay: [
{
id: "text-track",
type: "text",
elements: [
{
id: "text-1",
type: "text",
name: "Text",
content: "Hello",
fontSize: 20,
fontFamily: "Inter",
color: "#eeeeee",
textAlign: "left",
fontWeight: "bold",
fontStyle: "italic",
textDecoration: "underline",
letterSpacing: 1.5,
lineHeight: 1.4,
background: {
enabled: true,
color: "#111111",
paddingX: 10,
paddingY: 12,
},
transform: {
position: { x: 1, y: 2 },
scaleX: 1,
scaleY: 1,
rotate: 0,
},
opacity: 1,
},
],
},
],
audio: [],
},
},
],
},
});
expect(result.skipped).toBe(false);
expect(result.project.version).toBe(29);
const scene = asRecordArray(result.project.scenes)[0];
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.params).toEqual({
"transform.positionX": 12,
"transform.positionY": -4,
"transform.scaleX": 1.25,
"transform.scaleY": 0.75,
"transform.rotate": 9,
opacity: 0.5,
blendMode: "multiply",
volume: -6,
muted: true,
});
const overlay = asRecordArray(tracks.overlay)[0];
const text = asRecordArray(asRecord(overlay).elements)[0];
expect(text.content).toBeUndefined();
expect(text.background).toBeUndefined();
expect(text.params).toMatchObject({
content: "Hello",
fontSize: 20,
fontFamily: "Inter",
color: "#eeeeee",
textAlign: "left",
fontWeight: "bold",
fontStyle: "italic",
textDecoration: "underline",
letterSpacing: 1.5,
lineHeight: 1.4,
"background.enabled": true,
"background.color": "#111111",
"background.paddingX": 10,
"background.paddingY": 12,
"transform.positionX": 1,
"transform.positionY": 2,
});
});
});
@@ -27,10 +27,11 @@ import { V24toV25Migration } from "./v24-to-v25";
import { V25toV26Migration } from "./v25-to-v26";
import { V26toV27Migration } from "./v26-to-v27";
import { V27toV28Migration } from "./v27-to-v28";
import { V28toV29Migration } from "./v28-to-v29";
export { runStorageMigrations } from "./runner";
export type { MigrationProgress } from "./runner";
export const CURRENT_PROJECT_VERSION = 28;
export const CURRENT_PROJECT_VERSION = 29;
export const migrations = [
new V0toV1Migration(),
@@ -61,4 +62,5 @@ export const migrations = [
new V25toV26Migration(),
new V26toV27Migration(),
new V27toV28Migration(),
new V28toV29Migration(),
];
@@ -4,4 +4,5 @@ export { transformProjectV2ToV3 } from "./v2-to-v3";
export { transformProjectV3ToV4 } from "./v3-to-v4";
export { transformProjectV4ToV5 } from "./v4-to-v5";
export { transformProjectV17ToV18 } from "./v17-to-v18";
export { transformProjectV28ToV29 } from "./v28-to-v29";
export type { MigrationResult, ProjectRecord } from "./types";
@@ -0,0 +1,243 @@
import type { MigrationResult, ProjectRecord } from "./types";
import { getProjectId, isRecord } from "./utils";
export function transformProjectV28ToV29({
project,
}: {
project: ProjectRecord;
}): MigrationResult<ProjectRecord> {
if (!getProjectId({ project })) {
return { project, skipped: true, reason: "no project id" };
}
const version = project.version;
if (typeof version !== "number") {
return { project, skipped: true, reason: "invalid version" };
}
if (version >= 29) {
return { project, skipped: true, reason: "already v29" };
}
if (version !== 28) {
return { project, skipped: true, reason: "not v28" };
}
return {
project: {
...migrateProject({ project }),
version: 29,
},
skipped: false,
};
}
function migrateProject({ project }: { project: ProjectRecord }): ProjectRecord {
const nextProject = { ...project };
if (Array.isArray(project.scenes)) {
nextProject.scenes = project.scenes.map((scene) => migrateScene({ scene }));
}
return nextProject;
}
function migrateScene({ scene }: { scene: unknown }): unknown {
if (!isRecord(scene)) {
return scene;
}
const nextScene = { ...scene };
if (isRecord(scene.tracks)) {
nextScene.tracks = migrateTracks({ tracks: scene.tracks });
}
return nextScene;
}
function migrateTracks({ tracks }: { tracks: ProjectRecord }): ProjectRecord {
const nextTracks = { ...tracks };
if (isRecord(tracks.main)) {
nextTracks.main = migrateTrack({ track: tracks.main });
}
if (Array.isArray(tracks.overlay)) {
nextTracks.overlay = tracks.overlay.map((track) => migrateTrack({ track }));
}
if (Array.isArray(tracks.audio)) {
nextTracks.audio = tracks.audio.map((track) => migrateTrack({ track }));
}
return nextTracks;
}
function migrateTrack({ track }: { track: unknown }): unknown {
if (!isRecord(track) || !Array.isArray(track.elements)) {
return track;
}
return {
...track,
elements: track.elements.map((element) => migrateElement({ element })),
};
}
function migrateElement({ element }: { element: unknown }): unknown {
if (!isRecord(element)) {
return element;
}
const nextElement = { ...element };
const params: ProjectRecord = isRecord(element.params)
? { ...element.params }
: {};
copyTransformParams({ source: element, params });
copyPrimitiveParam({ source: element, params, sourceKey: "opacity", paramKey: "opacity" });
copyPrimitiveParam({
source: element,
params,
sourceKey: "blendMode",
paramKey: "blendMode",
});
if (element.type === "audio" || element.type === "video") {
copyPrimitiveParam({
source: element,
params,
sourceKey: "volume",
paramKey: "volume",
});
copyPrimitiveParam({
source: element,
params,
sourceKey: "muted",
paramKey: "muted",
});
}
if (element.type === "text") {
copyTextParams({ source: element, params });
}
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;
}
function copyTransformParams({
source,
params,
}: {
source: ProjectRecord;
params: ProjectRecord;
}): void {
if (!isRecord(source.transform)) {
return;
}
if (isRecord(source.transform.position)) {
copyPrimitiveParam({
source: source.transform.position,
params,
sourceKey: "x",
paramKey: "transform.positionX",
});
copyPrimitiveParam({
source: source.transform.position,
params,
sourceKey: "y",
paramKey: "transform.positionY",
});
}
copyPrimitiveParam({
source: source.transform,
params,
sourceKey: "scaleX",
paramKey: "transform.scaleX",
});
copyPrimitiveParam({
source: source.transform,
params,
sourceKey: "scaleY",
paramKey: "transform.scaleY",
});
copyPrimitiveParam({
source: source.transform,
params,
sourceKey: "rotate",
paramKey: "transform.rotate",
});
}
function copyTextParams({
source,
params,
}: {
source: ProjectRecord;
params: ProjectRecord;
}): void {
for (const key of [
"content",
"fontSize",
"fontFamily",
"color",
"textAlign",
"fontWeight",
"fontStyle",
"textDecoration",
"letterSpacing",
"lineHeight",
]) {
copyPrimitiveParam({ source, params, sourceKey: key, paramKey: key });
}
if (!isRecord(source.background)) {
return;
}
for (const key of [
"enabled",
"color",
"cornerRadius",
"paddingX",
"paddingY",
"offsetX",
"offsetY",
]) {
copyPrimitiveParam({
source: source.background,
params,
sourceKey: key,
paramKey: `background.${key}`,
});
}
}
function copyPrimitiveParam({
source,
params,
sourceKey,
paramKey,
}: {
source: ProjectRecord;
params: ProjectRecord;
sourceKey: string;
paramKey: string;
}): void {
const value = source[sourceKey];
if (
typeof value === "number" ||
typeof value === "string" ||
typeof value === "boolean"
) {
params[paramKey] = value;
}
}
@@ -0,0 +1,14 @@
import { StorageMigration, type StorageMigrationRunArgs } from "./base";
import type { MigrationResult, ProjectRecord } from "./transformers/types";
import { transformProjectV28ToV29 } from "./transformers/v28-to-v29";
export class V28toV29Migration extends StorageMigration {
from = 28;
to = 29;
async run({
project,
}: StorageMigrationRunArgs): Promise<MigrationResult<ProjectRecord>> {
return transformProjectV28ToV29({ project });
}
}