mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
refactor: new parameters modulerefactor: unify element params
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user