summaryrefslogtreecommitdiff
path: root/apps/web/src/lib
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-03-31 04:03:52 +0200
committerMaze Winther <mazewinther@gmail.com>2026-03-31 04:03:52 +0200
commitdcf54c1250431f5a4a2848f7c07781ef5eebacaf (patch)
tree64270f82d879ce0b0a028384e513ea87940da630 /apps/web/src/lib
parent5cc6abc3a44c9d046a1cb71a8c916f1d35827e8f (diff)
downloadopencut-dcf54c1250431f5a4a2848f7c07781ef5eebacaf.tar.gz
feat: migrate time utilities from typescript to rust WASM
Diffstat (limited to 'apps/web/src/lib')
-rw-r--r--apps/web/src/lib/time.ts225
-rw-r--r--apps/web/src/lib/timeline/bookmarks.ts298
2 files changed, 149 insertions, 374 deletions
diff --git a/apps/web/src/lib/time.ts b/apps/web/src/lib/time.ts
deleted file mode 100644
index aff42bee..00000000
--- a/apps/web/src/lib/time.ts
+++ /dev/null
@@ -1,225 +0,0 @@
-export type TTimeCode =
- | "MM:SS"
- | "HH:MM:SS"
- | "HH:MM:SS:CS"
- | "HH:MM:SS:FF";
-
-export function roundToFrame({
- time,
- fps,
-}: {
- time: number;
- fps: number;
-}): number {
- return Math.round(time * fps) / fps;
-}
-
-export function formatTimeCode({
- timeInSeconds,
- format = "HH:MM:SS:CS",
- fps,
-}: {
- timeInSeconds: number;
- format?: TTimeCode;
- fps?: number;
-}): string {
- const hours = Math.floor(timeInSeconds / 3600);
- const minutes = Math.floor((timeInSeconds % 3600) / 60);
- const seconds = Math.floor(timeInSeconds % 60);
- const centiseconds = Math.floor((timeInSeconds % 1) * 100);
- const frames = fps ? Math.floor((timeInSeconds % 1) * fps) : 0;
-
- switch (format) {
- case "MM:SS":
- return `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
- case "HH:MM:SS":
- return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
- case "HH:MM:SS:CS":
- return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}:${centiseconds.toString().padStart(2, "0")}`;
- case "HH:MM:SS:FF":
- if (!fps) throw new Error("FPS is required for HH:MM:SS:FF format");
- return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}:${frames.toString().padStart(2, "0")}`;
- }
-}
-
-export function parseTimeCode({
- timeCode,
- format = "HH:MM:SS:CS",
- fps,
-}: {
- timeCode: string;
- format?: TTimeCode;
- fps: number;
-}): number | null {
- if (!timeCode || typeof timeCode !== "string") return null;
-
- const cleanTimeCode = timeCode.trim();
-
- try {
- switch (format) {
- case "MM:SS": {
- const parts = cleanTimeCode.split(":");
- if (parts.length !== 2) return null;
- const [minutes, seconds] = parts.map((part) => parseInt(part, 10));
- if (Number.isNaN(minutes) || Number.isNaN(seconds)) return null;
- if (minutes < 0 || seconds < 0 || seconds >= 60) return null;
- return minutes * 60 + seconds;
- }
-
- case "HH:MM:SS": {
- const parts = cleanTimeCode.split(":");
- if (parts.length !== 3) return null;
- const [hours, minutes, seconds] = parts.map((part) =>
- parseInt(part, 10),
- );
- if (
- Number.isNaN(hours) ||
- Number.isNaN(minutes) ||
- Number.isNaN(seconds)
- )
- return null;
- if (
- hours < 0 ||
- minutes < 0 ||
- seconds < 0 ||
- minutes >= 60 ||
- seconds >= 60
- )
- return null;
- return hours * 3600 + minutes * 60 + seconds;
- }
-
- case "HH:MM:SS:CS": {
- const parts = cleanTimeCode.split(":");
- if (parts.length !== 4) return null;
- const [hours, minutes, seconds, centiseconds] = parts.map((part) =>
- parseInt(part, 10),
- );
- if (
- Number.isNaN(hours) ||
- Number.isNaN(minutes) ||
- Number.isNaN(seconds) ||
- Number.isNaN(centiseconds)
- )
- return null;
- if (
- hours < 0 ||
- minutes < 0 ||
- seconds < 0 ||
- centiseconds < 0 ||
- minutes >= 60 ||
- seconds >= 60 ||
- centiseconds >= 100
- )
- return null;
- return hours * 3600 + minutes * 60 + seconds + centiseconds / 100;
- }
-
- case "HH:MM:SS:FF": {
- const parts = cleanTimeCode.split(":");
- if (parts.length !== 4) return null;
- const [hours, minutes, seconds, frames] = parts.map((part) =>
- parseInt(part, 10),
- );
- if (
- Number.isNaN(hours) ||
- Number.isNaN(minutes) ||
- Number.isNaN(seconds) ||
- Number.isNaN(frames)
- )
- return null;
- if (
- hours < 0 ||
- minutes < 0 ||
- seconds < 0 ||
- frames < 0 ||
- minutes >= 60 ||
- seconds >= 60 ||
- frames >= fps
- )
- return null;
- return hours * 3600 + minutes * 60 + seconds + frames / fps;
- }
- }
- } catch {
- return null;
- }
-}
-
-export function guessTimeCodeFormat({
- timeCode,
-}: {
- timeCode: string;
-}): TTimeCode | null {
- if (!timeCode || typeof timeCode !== "string") return null;
-
- const numbers = timeCode.split(":");
-
- if (!numbers.every((n) => !Number.isNaN(Number(n)))) return null;
-
- if (numbers.length === 2) return "MM:SS";
- if (numbers.length === 3) return "HH:MM:SS";
- // todo: how to tell frames apart from cs?
- if (numbers.length === 4) return "HH:MM:SS:FF";
-
- return null;
-}
-
-export function timeToFrame({
- time,
- fps,
-}: {
- time: number;
- fps: number;
-}): number {
- return Math.round(time * fps);
-}
-
-export function frameToTime({
- frame,
- fps,
-}: {
- frame: number;
- fps: number;
-}): number {
- return frame / fps;
-}
-
-export function snapTimeToFrame({
- time,
- fps,
-}: {
- time: number;
- fps: number;
-}): number {
- if (fps <= 0) return time;
- const frame = timeToFrame({ time, fps });
- return frameToTime({ frame, fps });
-}
-
-export function getSnappedSeekTime({
- rawTime,
- duration,
- fps,
-}: {
- rawTime: number;
- duration: number;
- fps: number;
-}): number {
- const snappedTime = snapTimeToFrame({ time: rawTime, fps });
- const lastFrame = getLastFrameTime({ duration, fps });
- return Math.max(0, Math.min(lastFrame, snappedTime));
-}
-
-export function getLastFrameTime({
- duration,
- fps,
-}: {
- duration: number;
- fps: number;
-}): number {
- if (duration <= 0) return 0;
- if (fps <= 0) return duration;
- const frameOffset = 1 / fps;
- return Math.max(0, duration - frameOffset);
-}
diff --git a/apps/web/src/lib/timeline/bookmarks.ts b/apps/web/src/lib/timeline/bookmarks.ts
index 31bf73af..70d1b5ee 100644
--- a/apps/web/src/lib/timeline/bookmarks.ts
+++ b/apps/web/src/lib/timeline/bookmarks.ts
@@ -1,149 +1,149 @@
-import type { Bookmark } from "@/lib/timeline";
-import { roundToFrame } from "@/lib/time";
-
-export const BOOKMARK_TIME_EPSILON = 0.001;
-
-function bookmarkTimeEqual({
- bookmarkTime,
- frameTime,
-}: {
- bookmarkTime: number;
- frameTime: number;
-}): boolean {
- return Math.abs(bookmarkTime - frameTime) < BOOKMARK_TIME_EPSILON;
-}
-
-export function findBookmarkIndex({
- bookmarks,
- frameTime,
-}: {
- bookmarks: Bookmark[];
- frameTime: number;
-}): number {
- return bookmarks.findIndex((bookmark) =>
- bookmarkTimeEqual({ bookmarkTime: bookmark.time, frameTime }),
- );
-}
-
-export function isBookmarkAtTime({
- bookmarks,
- frameTime,
-}: {
- bookmarks: Bookmark[];
- frameTime: number;
-}): boolean {
- return bookmarks.some((bookmark) =>
- bookmarkTimeEqual({ bookmarkTime: bookmark.time, frameTime }),
- );
-}
-
-export function toggleBookmarkInArray({
- bookmarks,
- frameTime,
-}: {
- bookmarks: Bookmark[];
- frameTime: number;
-}): Bookmark[] {
- const bookmarkIndex = findBookmarkIndex({ bookmarks, frameTime });
-
- if (bookmarkIndex !== -1) {
- return bookmarks.filter((_, index) => index !== bookmarkIndex);
- }
-
- const newBookmarks = [...bookmarks, { time: frameTime }];
- return newBookmarks.slice().sort((a, b) => a.time - b.time);
-}
-
-export function removeBookmarkFromArray({
- bookmarks,
- frameTime,
-}: {
- bookmarks: Bookmark[];
- frameTime: number;
-}): Bookmark[] {
- return bookmarks.filter(
- (bookmark) =>
- !bookmarkTimeEqual({ bookmarkTime: bookmark.time, frameTime }),
- );
-}
-
-export function updateBookmarkInArray({
- bookmarks,
- frameTime,
- updates,
-}: {
- bookmarks: Bookmark[];
- frameTime: number;
- updates: Partial<Omit<Bookmark, "time">>;
-}): Bookmark[] {
- const index = findBookmarkIndex({ bookmarks, frameTime });
- if (index === -1) {
- return bookmarks;
- }
-
- const updated = { ...bookmarks[index], ...updates };
- const result = [...bookmarks];
- result[index] = updated;
- return result;
-}
-
-export function moveBookmarkInArray({
- bookmarks,
- fromTime,
- toTime,
-}: {
- bookmarks: Bookmark[];
- fromTime: number;
- toTime: number;
-}): Bookmark[] {
- const index = findBookmarkIndex({ bookmarks, frameTime: fromTime });
- if (index === -1) {
- return bookmarks;
- }
-
- const updated = { ...bookmarks[index], time: toTime };
- const result = [...bookmarks];
- result[index] = updated;
- return result.slice().sort((a, b) => a.time - b.time);
-}
-
-export function getFrameTime({
- time,
- fps,
-}: {
- time: number;
- fps: number;
-}): number {
- return roundToFrame({ time, fps });
-}
-
-export function getBookmarkAtTime({
- bookmarks,
- frameTime,
-}: {
- bookmarks: Bookmark[];
- frameTime: number;
-}): Bookmark | null {
- const index = findBookmarkIndex({ bookmarks, frameTime });
- return index === -1 ? null : bookmarks[index];
-}
-
-export function getBookmarksActiveAtTime({
- bookmarks,
- time,
-}: {
- bookmarks: Bookmark[];
- time: number;
-}): Bookmark[] {
- return bookmarks.filter((bookmark) => {
- const start = bookmark.time;
- const end =
- bookmark.duration != null && bookmark.duration > 0
- ? start + bookmark.duration
- : start;
- return (
- time >= start - BOOKMARK_TIME_EPSILON &&
- time <= end + BOOKMARK_TIME_EPSILON
- );
- });
-}
+import type { Bookmark } from "@/lib/timeline";
+import { roundToFrame } from "opencut-wasm";
+
+export const BOOKMARK_TIME_EPSILON = 0.001;
+
+function bookmarkTimeEqual({
+ bookmarkTime,
+ frameTime,
+}: {
+ bookmarkTime: number;
+ frameTime: number;
+}): boolean {
+ return Math.abs(bookmarkTime - frameTime) < BOOKMARK_TIME_EPSILON;
+}
+
+export function findBookmarkIndex({
+ bookmarks,
+ frameTime,
+}: {
+ bookmarks: Bookmark[];
+ frameTime: number;
+}): number {
+ return bookmarks.findIndex((bookmark) =>
+ bookmarkTimeEqual({ bookmarkTime: bookmark.time, frameTime }),
+ );
+}
+
+export function isBookmarkAtTime({
+ bookmarks,
+ frameTime,
+}: {
+ bookmarks: Bookmark[];
+ frameTime: number;
+}): boolean {
+ return bookmarks.some((bookmark) =>
+ bookmarkTimeEqual({ bookmarkTime: bookmark.time, frameTime }),
+ );
+}
+
+export function toggleBookmarkInArray({
+ bookmarks,
+ frameTime,
+}: {
+ bookmarks: Bookmark[];
+ frameTime: number;
+}): Bookmark[] {
+ const bookmarkIndex = findBookmarkIndex({ bookmarks, frameTime });
+
+ if (bookmarkIndex !== -1) {
+ return bookmarks.filter((_, index) => index !== bookmarkIndex);
+ }
+
+ const newBookmarks = [...bookmarks, { time: frameTime }];
+ return newBookmarks.slice().sort((a, b) => a.time - b.time);
+}
+
+export function removeBookmarkFromArray({
+ bookmarks,
+ frameTime,
+}: {
+ bookmarks: Bookmark[];
+ frameTime: number;
+}): Bookmark[] {
+ return bookmarks.filter(
+ (bookmark) =>
+ !bookmarkTimeEqual({ bookmarkTime: bookmark.time, frameTime }),
+ );
+}
+
+export function updateBookmarkInArray({
+ bookmarks,
+ frameTime,
+ updates,
+}: {
+ bookmarks: Bookmark[];
+ frameTime: number;
+ updates: Partial<Omit<Bookmark, "time">>;
+}): Bookmark[] {
+ const index = findBookmarkIndex({ bookmarks, frameTime });
+ if (index === -1) {
+ return bookmarks;
+ }
+
+ const updated = { ...bookmarks[index], ...updates };
+ const result = [...bookmarks];
+ result[index] = updated;
+ return result;
+}
+
+export function moveBookmarkInArray({
+ bookmarks,
+ fromTime,
+ toTime,
+}: {
+ bookmarks: Bookmark[];
+ fromTime: number;
+ toTime: number;
+}): Bookmark[] {
+ const index = findBookmarkIndex({ bookmarks, frameTime: fromTime });
+ if (index === -1) {
+ return bookmarks;
+ }
+
+ const updated = { ...bookmarks[index], time: toTime };
+ const result = [...bookmarks];
+ result[index] = updated;
+ return result.slice().sort((a, b) => a.time - b.time);
+}
+
+export function getFrameTime({
+ time,
+ fps,
+}: {
+ time: number;
+ fps: number;
+}): number {
+ return roundToFrame({ time, fps });
+}
+
+export function getBookmarkAtTime({
+ bookmarks,
+ frameTime,
+}: {
+ bookmarks: Bookmark[];
+ frameTime: number;
+}): Bookmark | null {
+ const index = findBookmarkIndex({ bookmarks, frameTime });
+ return index === -1 ? null : bookmarks[index];
+}
+
+export function getBookmarksActiveAtTime({
+ bookmarks,
+ time,
+}: {
+ bookmarks: Bookmark[];
+ time: number;
+}): Bookmark[] {
+ return bookmarks.filter((bookmark) => {
+ const start = bookmark.time;
+ const end =
+ bookmark.duration != null && bookmark.duration > 0
+ ? start + bookmark.duration
+ : start;
+ return (
+ time >= start - BOOKMARK_TIME_EPSILON &&
+ time <= end + BOOKMARK_TIME_EPSILON
+ );
+ });
+}