diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-04-07 01:09:13 +0200 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-04-07 01:09:13 +0200 |
| commit | e4b67094e757917588fb624a30aa046667403add (patch) | |
| tree | 55452692eac69a5a63b58f2a5cdb701dd7023d68 /apps/web/src/services/storage/migrations/transformers | |
| parent | 79df73643185c02493364a077402726d1fa7b9ec (diff) | |
| download | opencut-e4b67094e757917588fb624a30aa046667403add.tar.gz | |
refactor: split effects and masks into dedicated rust crates, introduce MediaTime and FrameRate
Diffstat (limited to 'apps/web/src/services/storage/migrations/transformers')
| -rw-r--r-- | apps/web/src/services/storage/migrations/transformers/v1-to-v2.ts | 2 | ||||
| -rw-r--r-- | apps/web/src/services/storage/migrations/transformers/v22-to-v23.ts | 335 |
2 files changed, 336 insertions, 1 deletions
diff --git a/apps/web/src/services/storage/migrations/transformers/v1-to-v2.ts b/apps/web/src/services/storage/migrations/transformers/v1-to-v2.ts index a2c72478..6313c311 100644 --- a/apps/web/src/services/storage/migrations/transformers/v1-to-v2.ts +++ b/apps/web/src/services/storage/migrations/transformers/v1-to-v2.ts @@ -3,7 +3,7 @@ import { DEFAULT_BACKGROUND_COLOR, } from "@/lib/background/constants"; import { DEFAULT_CANVAS_SIZE } from "@/lib/canvas/constants"; -import { DEFAULT_FPS } from "@/lib/fps/constants"; +const DEFAULT_FPS = 30; import { IndexedDBAdapter } from "@/services/storage/indexeddb-adapter"; import type { MediaAssetData } from "@/services/storage/types"; import type { MigrationResult, ProjectRecord } from "./types"; diff --git a/apps/web/src/services/storage/migrations/transformers/v22-to-v23.ts b/apps/web/src/services/storage/migrations/transformers/v22-to-v23.ts new file mode 100644 index 00000000..cde1294e --- /dev/null +++ b/apps/web/src/services/storage/migrations/transformers/v22-to-v23.ts @@ -0,0 +1,335 @@ +import type { MigrationResult, ProjectRecord } from "./types"; +import { getProjectId, isRecord } from "./utils"; + +import { TICKS_PER_SECOND } from "@/lib/wasm"; +const ARBITRARY_FPS_DENOMINATOR = 1_000_000; +const STANDARD_FRAME_RATES = [ + { value: 24_000 / 1_001, numerator: 24_000, denominator: 1_001 }, + { value: 24, numerator: 24, denominator: 1 }, + { value: 25, numerator: 25, denominator: 1 }, + { value: 30_000 / 1_001, numerator: 30_000, denominator: 1_001 }, + { value: 30, numerator: 30, denominator: 1 }, + { value: 48, numerator: 48, denominator: 1 }, + { value: 50, numerator: 50, denominator: 1 }, + { value: 60_000 / 1_001, numerator: 60_000, denominator: 1_001 }, + { value: 60, numerator: 60, denominator: 1 }, + { value: 120, numerator: 120, denominator: 1 }, +] as const; +const STANDARD_FRAME_RATE_TOLERANCE = 0.01; + +export function transformProjectV22ToV23({ + 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 >= 23) { + return { project, skipped: true, reason: "already v23" }; + } + if (version !== 22) { + return { project, skipped: true, reason: "not v22" }; + } + + return { + project: { + ...migrateProject({ project }), + version: 23, + }, + skipped: false, + }; +} + +function migrateProject({ + project, +}: { + project: ProjectRecord; +}): ProjectRecord { + const nextProject = { ...project }; + + if (isRecord(project.metadata)) { + nextProject.metadata = migrateMetadata({ metadata: project.metadata }); + } + + if (isRecord(project.settings)) { + nextProject.settings = migrateSettings({ settings: project.settings }); + } + + if (isRecord(project.timelineViewState)) { + nextProject.timelineViewState = migrateTimelineViewState({ + timelineViewState: project.timelineViewState, + }); + } + + if (Array.isArray(project.scenes)) { + nextProject.scenes = project.scenes.map((scene) => migrateScene({ scene })); + } + + return nextProject; +} + +function migrateMetadata({ + metadata, +}: { + metadata: ProjectRecord; +}): ProjectRecord { + return migrateTimeFields({ + record: metadata, + keys: ["duration"], + }); +} + +function migrateSettings({ + settings, +}: { + settings: ProjectRecord; +}): ProjectRecord { + const nextSettings = { ...settings }; + if ("fps" in settings) { + nextSettings.fps = migrateFrameRate({ fps: settings.fps }); + } + return nextSettings; +} + +function migrateTimelineViewState({ + timelineViewState, +}: { + timelineViewState: ProjectRecord; +}): ProjectRecord { + return migrateTimeFields({ + record: timelineViewState, + keys: ["playheadTime"], + }); +} + +function migrateScene({ scene }: { scene: unknown }): unknown { + if (!isRecord(scene)) { + return scene; + } + + const nextScene = { ...scene }; + + if (Array.isArray(scene.bookmarks)) { + nextScene.bookmarks = scene.bookmarks.map((bookmark) => + migrateBookmark({ bookmark }), + ); + } + + if (Array.isArray(scene.tracks)) { + nextScene.tracks = scene.tracks.map((track) => migrateTrack({ track })); + } + + return nextScene; +} + +function migrateTrack({ track }: { track: unknown }): unknown { + if (!isRecord(track)) { + return track; + } + + if (!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 = migrateTimeFields({ + record: element, + keys: ["duration", "startTime", "trimStart", "trimEnd", "sourceDuration"], + }); + + if (isRecord(element.animations)) { + nextElement.animations = migrateAnimations({ + animations: element.animations, + }); + } + + return nextElement; +} + +function migrateAnimations({ + animations, +}: { + animations: ProjectRecord; +}): ProjectRecord { + if (!isRecord(animations.channels)) { + return animations; + } + + return { + ...animations, + channels: Object.fromEntries( + Object.entries(animations.channels).map(([channelId, channel]) => [ + channelId, + migrateAnimationChannel({ channel }), + ]), + ), + }; +} + +function migrateAnimationChannel({ channel }: { channel: unknown }): unknown { + if (!isRecord(channel)) { + return channel; + } + + if (!Array.isArray(channel.keys)) { + return channel; + } + + return { + ...channel, + keys: channel.keys.map((keyframe) => migrateAnimationKeyframe({ keyframe })), + }; +} + +function migrateAnimationKeyframe({ + keyframe, +}: { + keyframe: unknown; +}): unknown { + if (!isRecord(keyframe)) { + return keyframe; + } + + const nextKeyframe = migrateTimeFields({ + record: keyframe, + keys: ["time"], + }); + + if (isRecord(keyframe.leftHandle)) { + nextKeyframe.leftHandle = migrateCurveHandle({ + handle: keyframe.leftHandle, + }); + } + + if (isRecord(keyframe.rightHandle)) { + nextKeyframe.rightHandle = migrateCurveHandle({ + handle: keyframe.rightHandle, + }); + } + + return nextKeyframe; +} + +function migrateCurveHandle({ + handle, +}: { + handle: ProjectRecord; +}): ProjectRecord { + return migrateTimeFields({ + record: handle, + keys: ["dt"], + }); +} + +function migrateBookmark({ bookmark }: { bookmark: unknown }): unknown { + if (!isRecord(bookmark)) { + return bookmark; + } + + return migrateTimeFields({ + record: bookmark, + keys: ["time", "duration"], + }); +} + +function migrateTimeFields({ + record, + keys, +}: { + record: ProjectRecord; + keys: string[]; +}): ProjectRecord { + const nextRecord = { ...record }; + + for (const key of keys) { + if (!(key in record)) { + continue; + } + + nextRecord[key] = migrateTimeValue({ value: record[key] }); + } + + return nextRecord; +} + +function migrateTimeValue({ value }: { value: unknown }): unknown { + if (typeof value !== "number" || !Number.isFinite(value)) { + return value; + } + + return secondsToTicks({ value }); +} + +function secondsToTicks({ value }: { value: number }): number { + return Math.round(value * TICKS_PER_SECOND); +} + +function migrateFrameRate({ fps }: { fps: unknown }): unknown { + if (isRecord(fps)) { + return fps; + } + + if (typeof fps !== "number" || !Number.isFinite(fps) || fps <= 0) { + return fps; + } + + const standardFrameRate = STANDARD_FRAME_RATES.find( + (candidate) => Math.abs(fps - candidate.value) <= STANDARD_FRAME_RATE_TOLERANCE, + ); + if (standardFrameRate) { + return { + numerator: standardFrameRate.numerator, + denominator: standardFrameRate.denominator, + }; + } + + if (Number.isInteger(fps)) { + return { numerator: fps, denominator: 1 }; + } + + const scaledNumerator = Math.round(fps * ARBITRARY_FPS_DENOMINATOR); + const divisor = greatestCommonDivisor({ + left: scaledNumerator, + right: ARBITRARY_FPS_DENOMINATOR, + }); + + return { + numerator: scaledNumerator / divisor, + denominator: ARBITRARY_FPS_DENOMINATOR / divisor, + }; +} + +function greatestCommonDivisor({ + left, + right, +}: { + left: number; + right: number; +}): number { + let nextLeft = Math.abs(left); + let nextRight = Math.abs(right); + + while (nextRight !== 0) { + const remainder = nextLeft % nextRight; + nextLeft = nextRight; + nextRight = remainder; + } + + return nextLeft || 1; +} |
