summaryrefslogtreecommitdiff
path: root/apps/web/src/services/storage/migrations/transformers/v11-to-v12.ts
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-03-25 16:47:22 +0100
committerMaze Winther <mazewinther@gmail.com>2026-03-29 15:48:22 +0200
commit8db3bead13441a5882d8689dfc6a9f09c82375d9 (patch)
treee9d0cafa93904ffcbec3188c37961bcc16c6422d /apps/web/src/services/storage/migrations/transformers/v11-to-v12.ts
parent39ea298a9ccba3104c549c0ca079c231981811f5 (diff)
downloadopencut-8db3bead13441a5882d8689dfc6a9f09c82375d9.tar.gz
feat: masks, properties refactor, shaders, storage migrations, and more
Diffstat (limited to 'apps/web/src/services/storage/migrations/transformers/v11-to-v12.ts')
-rw-r--r--apps/web/src/services/storage/migrations/transformers/v11-to-v12.ts196
1 files changed, 196 insertions, 0 deletions
diff --git a/apps/web/src/services/storage/migrations/transformers/v11-to-v12.ts b/apps/web/src/services/storage/migrations/transformers/v11-to-v12.ts
new file mode 100644
index 00000000..fa1bd371
--- /dev/null
+++ b/apps/web/src/services/storage/migrations/transformers/v11-to-v12.ts
@@ -0,0 +1,196 @@
+import type { MigrationResult, ProjectRecord } from "./types";
+import { getProjectId, isRecord } from "./utils";
+
+export function transformProjectV11ToV12({
+ project,
+}: {
+ project: ProjectRecord;
+}): MigrationResult<ProjectRecord> {
+ if (!getProjectId({ project })) {
+ return { project, skipped: true, reason: "no project id" };
+ }
+
+ if (typeof project.version === "number" && project.version >= 12) {
+ return { project, skipped: true, reason: "already v12" };
+ }
+
+ const migratedProject = migrateProjectPosition({ project });
+
+ return {
+ project: { ...migratedProject, version: 12 },
+ skipped: false,
+ };
+}
+
+function migrateProjectPosition({
+ project,
+}: {
+ project: ProjectRecord;
+}): ProjectRecord {
+ const scenesValue = project.scenes;
+ if (!Array.isArray(scenesValue)) return project;
+
+ const migratedScenes = scenesValue.map((scene) =>
+ migrateScenePosition({ scene }),
+ );
+
+ return { ...project, scenes: migratedScenes };
+}
+
+function migrateScenePosition({ scene }: { scene: unknown }): unknown {
+ if (!isRecord(scene)) return scene;
+
+ const tracksValue = scene.tracks;
+ if (!Array.isArray(tracksValue)) return scene;
+
+ const migratedTracks = tracksValue.map((track) =>
+ migrateTrackPosition({ track }),
+ );
+
+ return { ...scene, tracks: migratedTracks };
+}
+
+function migrateTrackPosition({ track }: { track: unknown }): unknown {
+ if (!isRecord(track)) return track;
+
+ const elementsValue = track.elements;
+ if (!Array.isArray(elementsValue)) return track;
+
+ const migratedElements = elementsValue.map((element) =>
+ migrateElementPosition({ element }),
+ );
+
+ return { ...track, elements: migratedElements };
+}
+
+function migrateElementPosition({ element }: { element: unknown }): unknown {
+ if (!isRecord(element)) return element;
+
+ const animations = element.animations;
+ if (!isRecord(animations) || !isRecord(animations.channels)) return element;
+
+ const channels = animations.channels as Record<string, unknown>;
+ const xChannel = channels["transform.position.x"];
+ const yChannel = channels["transform.position.y"];
+
+ if (!xChannel && !yChannel) return element;
+
+ const baseTransform = isRecord(element.transform) ? element.transform : {};
+ const basePosition = isRecord(baseTransform.position)
+ ? baseTransform.position
+ : {};
+ const baseX = typeof basePosition.x === "number" ? basePosition.x : 0;
+ const baseY = typeof basePosition.y === "number" ? basePosition.y : 0;
+
+ const xKeyframes = getKeyframes({ channel: xChannel });
+ const yKeyframes = getKeyframes({ channel: yChannel });
+
+ const allTimes = Array.from(
+ new Set([
+ ...xKeyframes.map((kf) => kf.time),
+ ...yKeyframes.map((kf) => kf.time),
+ ]),
+ ).sort((a, b) => a - b);
+
+ const vectorKeyframes = allTimes.map((time) => {
+ const xKf = xKeyframes.find((kf) => Math.abs(kf.time - time) < 0.001);
+ const yKf = yKeyframes.find((kf) => Math.abs(kf.time - time) < 0.001);
+ const x =
+ typeof xKf?.value === "number"
+ ? xKf.value
+ : interpolateScalarAtTime({
+ keyframes: xKeyframes,
+ time,
+ fallback: baseX,
+ });
+ const y =
+ typeof yKf?.value === "number"
+ ? yKf.value
+ : interpolateScalarAtTime({
+ keyframes: yKeyframes,
+ time,
+ fallback: baseY,
+ });
+ const interpolation = xKf?.interpolation ?? yKf?.interpolation ?? "linear";
+ return {
+ id: xKf?.id ?? yKf?.id ?? crypto.randomUUID(),
+ time,
+ value: { x, y },
+ interpolation,
+ };
+ });
+
+ const newChannels = { ...channels };
+ delete newChannels["transform.position.x"];
+ delete newChannels["transform.position.y"];
+ newChannels["transform.position"] = {
+ valueKind: "vector",
+ keyframes: vectorKeyframes,
+ };
+
+ return {
+ ...element,
+ animations: { ...animations, channels: newChannels },
+ };
+}
+
+interface ScalarKeyframe {
+ id: string;
+ time: number;
+ value: number;
+ interpolation: string;
+}
+
+function getKeyframes({ channel }: { channel: unknown }): ScalarKeyframe[] {
+ if (!isRecord(channel)) return [];
+ if (!Array.isArray(channel.keyframes)) return [];
+ return channel.keyframes.flatMap((kf) => {
+ if (!isRecord(kf)) return [];
+ if (
+ typeof kf.id !== "string" ||
+ typeof kf.time !== "number" ||
+ typeof kf.value !== "number"
+ )
+ return [];
+ return [
+ {
+ id: kf.id,
+ time: kf.time,
+ value: kf.value,
+ interpolation:
+ typeof kf.interpolation === "string" ? kf.interpolation : "linear",
+ },
+ ];
+ });
+}
+
+function interpolateScalarAtTime({
+ keyframes,
+ time,
+ fallback,
+}: {
+ keyframes: ScalarKeyframe[];
+ time: number;
+ fallback: number;
+}): number {
+ if (keyframes.length === 0) return fallback;
+
+ const sorted = [...keyframes].sort((a, b) => a.time - b.time);
+ const first = sorted[0];
+ const last = sorted[sorted.length - 1];
+
+ if (!first || !last) return fallback;
+ if (time <= first.time) return first.value;
+ if (time >= last.time) return last.value;
+
+ for (let i = 0; i < sorted.length - 1; i++) {
+ const left = sorted[i];
+ const right = sorted[i + 1];
+ if (time < left.time || time > right.time) continue;
+ if (left.interpolation === "hold") return left.value;
+ const t = (time - left.time) / (right.time - left.time);
+ return left.value + (right.value - left.value) * t;
+ }
+
+ return last.value;
+}