diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-03-25 16:47:22 +0100 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-03-29 15:48:22 +0200 |
| commit | 8db3bead13441a5882d8689dfc6a9f09c82375d9 (patch) | |
| tree | e9d0cafa93904ffcbec3188c37961bcc16c6422d /apps/web/src/services/storage/migrations/transformers/v11-to-v12.ts | |
| parent | 39ea298a9ccba3104c549c0ca079c231981811f5 (diff) | |
| download | opencut-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.ts | 196 |
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; +} |
