feat: migrate time utilities from typescript to rust WASM

This commit is contained in:
Maze Winther
2026-03-31 04:03:52 +02:00
parent 5cc6abc3a4
commit dcf54c1250
29 changed files with 2008 additions and 2025 deletions
-225
View File
@@ -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);
}
+149 -149
View File
@@ -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
);
});
}