mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
feat: migrate time utilities from typescript to rust WASM
This commit is contained in:
@@ -9,7 +9,9 @@ crate-type = ["rlib", "cdylib"]
|
||||
|
||||
[dependencies]
|
||||
bridge = { version = "0.1.0", path = "../bridge" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
tsify-next = { version = "0.5", optional = true }
|
||||
wasm-bindgen = { version = "0.2.115", optional = true }
|
||||
|
||||
[features]
|
||||
wasm = ["dep:wasm-bindgen"]
|
||||
wasm = ["dep:wasm-bindgen", "dep:tsify-next", "tsify-next/js"]
|
||||
+223
-75
@@ -1,52 +1,129 @@
|
||||
use bridge::export;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const DEFAULT_TIME_CODE_FORMAT: &str = "HH:MM:SS:CS";
|
||||
const SECONDS_PER_HOUR: f64 = 3600.0;
|
||||
const SECONDS_PER_MINUTE: f64 = 60.0;
|
||||
const CENTISECONDS_PER_SECOND: f64 = 100.0;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum TimeCodeFormat {
|
||||
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
|
||||
#[cfg_attr(feature = "wasm", tsify(from_wasm_abi, into_wasm_abi))]
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum TimeCodeFormat {
|
||||
#[serde(rename = "MM:SS")]
|
||||
MmSs,
|
||||
#[serde(rename = "HH:MM:SS")]
|
||||
HhMmSs,
|
||||
#[serde(rename = "HH:MM:SS:CS")]
|
||||
HhMmSsCs,
|
||||
#[serde(rename = "HH:MM:SS:FF")]
|
||||
HhMmSsFf,
|
||||
}
|
||||
|
||||
impl TimeCodeFormat {
|
||||
fn parse(format: Option<&str>) -> Option<Self> {
|
||||
match format.unwrap_or(DEFAULT_TIME_CODE_FORMAT) {
|
||||
"MM:SS" => Some(Self::MmSs),
|
||||
"HH:MM:SS" => Some(Self::HhMmSs),
|
||||
"HH:MM:SS:CS" => Some(Self::HhMmSsCs),
|
||||
"HH:MM:SS:FF" => Some(Self::HhMmSsFf),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
|
||||
#[cfg_attr(feature = "wasm", tsify(from_wasm_abi))]
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RoundToFrameOptions {
|
||||
pub time: f64,
|
||||
pub fps: f64,
|
||||
}
|
||||
|
||||
fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::MmSs => "MM:SS",
|
||||
Self::HhMmSs => "HH:MM:SS",
|
||||
Self::HhMmSsCs => "HH:MM:SS:CS",
|
||||
Self::HhMmSsFf => "HH:MM:SS:FF",
|
||||
}
|
||||
}
|
||||
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
|
||||
#[cfg_attr(feature = "wasm", tsify(from_wasm_abi))]
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FormatTimeCodeOptions {
|
||||
pub time_in_seconds: f64,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub format: Option<TimeCodeFormat>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub fps: Option<f64>,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
|
||||
#[cfg_attr(feature = "wasm", tsify(from_wasm_abi))]
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ParseTimeCodeOptions {
|
||||
pub time_code: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub format: Option<TimeCodeFormat>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub fps: Option<f64>,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
|
||||
#[cfg_attr(feature = "wasm", tsify(from_wasm_abi))]
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GuessTimeCodeFormatOptions {
|
||||
pub time_code: String,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
|
||||
#[cfg_attr(feature = "wasm", tsify(from_wasm_abi))]
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TimeToFrameOptions {
|
||||
pub time: f64,
|
||||
pub fps: f64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
|
||||
#[cfg_attr(feature = "wasm", tsify(from_wasm_abi))]
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FrameToTimeOptions {
|
||||
pub frame: f64,
|
||||
pub fps: f64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
|
||||
#[cfg_attr(feature = "wasm", tsify(from_wasm_abi))]
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SnapTimeToFrameOptions {
|
||||
pub time: f64,
|
||||
pub fps: f64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
|
||||
#[cfg_attr(feature = "wasm", tsify(from_wasm_abi))]
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GetSnappedSeekTimeOptions {
|
||||
pub raw_time: f64,
|
||||
pub duration: f64,
|
||||
pub fps: f64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
|
||||
#[cfg_attr(feature = "wasm", tsify(from_wasm_abi))]
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GetLastFrameTimeOptions {
|
||||
pub duration: f64,
|
||||
pub fps: f64,
|
||||
}
|
||||
|
||||
#[export]
|
||||
pub fn round_to_frame(time: f64, fps: f64) -> f64 {
|
||||
pub fn round_to_frame(RoundToFrameOptions { time, fps }: RoundToFrameOptions) -> f64 {
|
||||
(time * fps).round() / fps
|
||||
}
|
||||
|
||||
#[export]
|
||||
pub fn format_time_code(
|
||||
time_in_seconds: f64,
|
||||
format: Option<String>,
|
||||
fps: Option<f64>,
|
||||
FormatTimeCodeOptions {
|
||||
time_in_seconds,
|
||||
format,
|
||||
fps,
|
||||
}: FormatTimeCodeOptions,
|
||||
) -> Option<String> {
|
||||
let format = TimeCodeFormat::parse(format.as_deref())?;
|
||||
let hours = (time_in_seconds / 3600.0).floor() as u64;
|
||||
let minutes = ((time_in_seconds % 3600.0) / 60.0).floor() as u64;
|
||||
let seconds = (time_in_seconds % 60.0).floor() as u64;
|
||||
let centiseconds = ((time_in_seconds % 1.0) * 100.0).floor() as u64;
|
||||
let format = format.unwrap_or(TimeCodeFormat::HhMmSsCs);
|
||||
let hours = (time_in_seconds / SECONDS_PER_HOUR).floor() as u64;
|
||||
let minutes = ((time_in_seconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE).floor() as u64;
|
||||
let seconds = (time_in_seconds % SECONDS_PER_MINUTE).floor() as u64;
|
||||
let centiseconds = ((time_in_seconds % 1.0) * CENTISECONDS_PER_SECOND).floor() as u64;
|
||||
|
||||
match format {
|
||||
TimeCodeFormat::MmSs => Some(format!("{minutes:02}:{seconds:02}")),
|
||||
@@ -69,12 +146,18 @@ pub fn format_time_code(
|
||||
}
|
||||
|
||||
#[export]
|
||||
pub fn parse_time_code(time_code: &str, format: Option<String>, fps: Option<f64>) -> Option<f64> {
|
||||
pub fn parse_time_code(
|
||||
ParseTimeCodeOptions {
|
||||
time_code,
|
||||
format,
|
||||
fps,
|
||||
}: ParseTimeCodeOptions,
|
||||
) -> Option<f64> {
|
||||
if time_code.trim().is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let format = TimeCodeFormat::parse(format.as_deref())?;
|
||||
let format = format.unwrap_or(TimeCodeFormat::HhMmSsCs);
|
||||
let parts = time_code
|
||||
.trim()
|
||||
.split(':')
|
||||
@@ -86,35 +169,42 @@ pub fn parse_time_code(time_code: &str, format: Option<String>, fps: Option<f64>
|
||||
let [minutes, seconds] = parts.as_slice() else {
|
||||
return None;
|
||||
};
|
||||
if *seconds >= 60 {
|
||||
if *seconds >= SECONDS_PER_MINUTE as u32 {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some((*minutes as f64 * 60.0) + *seconds as f64)
|
||||
Some((*minutes as f64 * SECONDS_PER_MINUTE) + *seconds as f64)
|
||||
}
|
||||
TimeCodeFormat::HhMmSs => {
|
||||
let [hours, minutes, seconds] = parts.as_slice() else {
|
||||
return None;
|
||||
};
|
||||
if *minutes >= 60 || *seconds >= 60 {
|
||||
if *minutes >= SECONDS_PER_MINUTE as u32 || *seconds >= SECONDS_PER_MINUTE as u32 {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some((*hours as f64 * 3600.0) + (*minutes as f64 * 60.0) + *seconds as f64)
|
||||
Some(
|
||||
(*hours as f64 * SECONDS_PER_HOUR)
|
||||
+ (*minutes as f64 * SECONDS_PER_MINUTE)
|
||||
+ *seconds as f64,
|
||||
)
|
||||
}
|
||||
TimeCodeFormat::HhMmSsCs => {
|
||||
let [hours, minutes, seconds, centiseconds] = parts.as_slice() else {
|
||||
return None;
|
||||
};
|
||||
if *minutes >= 60 || *seconds >= 60 || *centiseconds >= 100 {
|
||||
if *minutes >= SECONDS_PER_MINUTE as u32
|
||||
|| *seconds >= SECONDS_PER_MINUTE as u32
|
||||
|| *centiseconds >= CENTISECONDS_PER_SECOND as u32
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(
|
||||
(*hours as f64 * 3600.0)
|
||||
+ (*minutes as f64 * 60.0)
|
||||
(*hours as f64 * SECONDS_PER_HOUR)
|
||||
+ (*minutes as f64 * SECONDS_PER_MINUTE)
|
||||
+ *seconds as f64
|
||||
+ (*centiseconds as f64 / 100.0),
|
||||
+ (*centiseconds as f64 / CENTISECONDS_PER_SECOND),
|
||||
)
|
||||
}
|
||||
TimeCodeFormat::HhMmSsFf => {
|
||||
@@ -126,13 +216,16 @@ pub fn parse_time_code(time_code: &str, format: Option<String>, fps: Option<f64>
|
||||
let [hours, minutes, seconds, frames] = parts.as_slice() else {
|
||||
return None;
|
||||
};
|
||||
if *minutes >= 60 || *seconds >= 60 || *frames as f64 >= fps {
|
||||
if *minutes >= SECONDS_PER_MINUTE as u32
|
||||
|| *seconds >= SECONDS_PER_MINUTE as u32
|
||||
|| *frames as f64 >= fps
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(
|
||||
(*hours as f64 * 3600.0)
|
||||
+ (*minutes as f64 * 60.0)
|
||||
(*hours as f64 * SECONDS_PER_HOUR)
|
||||
+ (*minutes as f64 * SECONDS_PER_MINUTE)
|
||||
+ *seconds as f64
|
||||
+ (*frames as f64 / fps),
|
||||
)
|
||||
@@ -141,7 +234,9 @@ pub fn parse_time_code(time_code: &str, format: Option<String>, fps: Option<f64>
|
||||
}
|
||||
|
||||
#[export]
|
||||
pub fn guess_time_code_format(time_code: &str) -> Option<String> {
|
||||
pub fn guess_time_code_format(
|
||||
GuessTimeCodeFormatOptions { time_code }: GuessTimeCodeFormatOptions,
|
||||
) -> Option<TimeCodeFormat> {
|
||||
if time_code.trim().is_empty() {
|
||||
return None;
|
||||
}
|
||||
@@ -153,41 +248,52 @@ pub fn guess_time_code_format(time_code: &str) -> Option<String> {
|
||||
})?;
|
||||
|
||||
match part_count {
|
||||
2 => Some(TimeCodeFormat::MmSs.as_str().to_string()),
|
||||
3 => Some(TimeCodeFormat::HhMmSs.as_str().to_string()),
|
||||
4 => Some(TimeCodeFormat::HhMmSsFf.as_str().to_string()),
|
||||
2 => Some(TimeCodeFormat::MmSs),
|
||||
3 => Some(TimeCodeFormat::HhMmSs),
|
||||
4 => Some(TimeCodeFormat::HhMmSsFf),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[export]
|
||||
pub fn time_to_frame(time: f64, fps: f64) -> f64 {
|
||||
pub fn time_to_frame(TimeToFrameOptions { time, fps }: TimeToFrameOptions) -> f64 {
|
||||
(time * fps).round()
|
||||
}
|
||||
|
||||
#[export]
|
||||
pub fn frame_to_time(frame: f64, fps: f64) -> f64 {
|
||||
pub fn frame_to_time(FrameToTimeOptions { frame, fps }: FrameToTimeOptions) -> f64 {
|
||||
frame / fps
|
||||
}
|
||||
|
||||
#[export]
|
||||
pub fn snap_time_to_frame(time: f64, fps: f64) -> f64 {
|
||||
pub fn snap_time_to_frame(SnapTimeToFrameOptions { time, fps }: SnapTimeToFrameOptions) -> f64 {
|
||||
if fps <= 0.0 {
|
||||
return time;
|
||||
}
|
||||
|
||||
frame_to_time(time_to_frame(time, fps), fps)
|
||||
frame_to_time(FrameToTimeOptions {
|
||||
frame: time_to_frame(TimeToFrameOptions { time, fps }),
|
||||
fps,
|
||||
})
|
||||
}
|
||||
|
||||
#[export]
|
||||
pub fn get_snapped_seek_time(raw_time: f64, duration: f64, fps: f64) -> f64 {
|
||||
let snapped_time = snap_time_to_frame(raw_time, fps);
|
||||
let last_frame = get_last_frame_time(duration, fps);
|
||||
pub fn get_snapped_seek_time(
|
||||
GetSnappedSeekTimeOptions {
|
||||
raw_time,
|
||||
duration,
|
||||
fps,
|
||||
}: GetSnappedSeekTimeOptions,
|
||||
) -> f64 {
|
||||
let snapped_time = snap_time_to_frame(SnapTimeToFrameOptions { time: raw_time, fps });
|
||||
let last_frame = get_last_frame_time(GetLastFrameTimeOptions { duration, fps });
|
||||
snapped_time.clamp(0.0, last_frame)
|
||||
}
|
||||
|
||||
#[export]
|
||||
pub fn get_last_frame_time(duration: f64, fps: f64) -> f64 {
|
||||
pub fn get_last_frame_time(
|
||||
GetLastFrameTimeOptions { duration, fps }: GetLastFrameTimeOptions,
|
||||
) -> f64 {
|
||||
if duration <= 0.0 {
|
||||
return 0.0;
|
||||
}
|
||||
@@ -206,18 +312,26 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn rounds_to_the_nearest_frame() {
|
||||
assert_eq!(round_to_frame(1.24, 10.0), 1.2);
|
||||
assert_eq!(round_to_frame(1.26, 10.0), 1.3);
|
||||
assert_eq!(round_to_frame(RoundToFrameOptions { time: 1.24, fps: 10.0 }), 1.2);
|
||||
assert_eq!(round_to_frame(RoundToFrameOptions { time: 1.26, fps: 10.0 }), 1.3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn formats_default_time_codes() {
|
||||
assert_eq!(
|
||||
format_time_code(3723.45, None, None),
|
||||
Some("01:02:03:44".to_string()),
|
||||
format_time_code(FormatTimeCodeOptions {
|
||||
time_in_seconds: 3723.45,
|
||||
format: None,
|
||||
fps: None,
|
||||
}),
|
||||
Some("01:02:03:44".to_string()),
|
||||
);
|
||||
assert_eq!(
|
||||
format_time_code(65.0, Some("MM:SS".to_string()), None),
|
||||
format_time_code(FormatTimeCodeOptions {
|
||||
time_in_seconds: 65.0,
|
||||
format: Some(TimeCodeFormat::MmSs),
|
||||
fps: None,
|
||||
}),
|
||||
Some("01:05".to_string()),
|
||||
);
|
||||
}
|
||||
@@ -225,11 +339,19 @@ mod tests {
|
||||
#[test]
|
||||
fn formats_frame_based_time_codes() {
|
||||
assert_eq!(
|
||||
format_time_code(1.5, Some("HH:MM:SS:FF".to_string()), Some(30.0)),
|
||||
format_time_code(FormatTimeCodeOptions {
|
||||
time_in_seconds: 1.5,
|
||||
format: Some(TimeCodeFormat::HhMmSsFf),
|
||||
fps: Some(30.0),
|
||||
}),
|
||||
Some("00:00:01:15".to_string()),
|
||||
);
|
||||
assert_eq!(
|
||||
format_time_code(1.5, Some("HH:MM:SS:FF".to_string()), None),
|
||||
format_time_code(FormatTimeCodeOptions {
|
||||
time_in_seconds: 1.5,
|
||||
format: Some(TimeCodeFormat::HhMmSsFf),
|
||||
fps: None,
|
||||
}),
|
||||
None,
|
||||
);
|
||||
}
|
||||
@@ -237,38 +359,64 @@ mod tests {
|
||||
#[test]
|
||||
fn parses_time_codes() {
|
||||
assert_eq!(
|
||||
parse_time_code("01:05", Some("MM:SS".to_string()), None),
|
||||
parse_time_code(ParseTimeCodeOptions {
|
||||
time_code: "01:05".to_string(),
|
||||
format: Some(TimeCodeFormat::MmSs),
|
||||
fps: None,
|
||||
}),
|
||||
Some(65.0),
|
||||
);
|
||||
assert_eq!(
|
||||
parse_time_code("00:00:01:15", Some("HH:MM:SS:FF".to_string()), Some(30.0)),
|
||||
parse_time_code(ParseTimeCodeOptions {
|
||||
time_code: "00:00:01:15".to_string(),
|
||||
format: Some(TimeCodeFormat::HhMmSsFf),
|
||||
fps: Some(30.0),
|
||||
}),
|
||||
Some(1.5),
|
||||
);
|
||||
assert_eq!(
|
||||
parse_time_code("00:00:01:30", Some("HH:MM:SS:FF".to_string()), Some(30.0)),
|
||||
parse_time_code(ParseTimeCodeOptions {
|
||||
time_code: "00:00:01:30".to_string(),
|
||||
format: Some(TimeCodeFormat::HhMmSsFf),
|
||||
fps: Some(30.0),
|
||||
}),
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guesses_time_code_formats() {
|
||||
assert_eq!(guess_time_code_format("01:05"), Some("MM:SS".to_string()));
|
||||
assert_eq!(
|
||||
guess_time_code_format("00:00:01"),
|
||||
Some("HH:MM:SS".to_string()),
|
||||
guess_time_code_format(GuessTimeCodeFormatOptions { time_code: "01:05".to_string() }),
|
||||
Some(TimeCodeFormat::MmSs),
|
||||
);
|
||||
assert_eq!(
|
||||
guess_time_code_format("00:00:01:15"),
|
||||
Some("HH:MM:SS:FF".to_string()),
|
||||
guess_time_code_format(GuessTimeCodeFormatOptions {
|
||||
time_code: "00:00:01".to_string(),
|
||||
}),
|
||||
Some(TimeCodeFormat::HhMmSs),
|
||||
);
|
||||
assert_eq!(
|
||||
guess_time_code_format(GuessTimeCodeFormatOptions {
|
||||
time_code: "00:00:01:15".to_string(),
|
||||
}),
|
||||
Some(TimeCodeFormat::HhMmSsFf),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snaps_and_clamps_seek_time() {
|
||||
assert_eq!(time_to_frame(1.26, 10.0), 13.0);
|
||||
assert_eq!(frame_to_time(13.0, 10.0), 1.3);
|
||||
assert_eq!(snap_time_to_frame(1.26, 10.0), 1.3);
|
||||
assert_eq!(get_last_frame_time(10.0, 5.0), 9.8);
|
||||
assert_eq!(get_snapped_seek_time(10.0, 10.0, 5.0), 9.8);
|
||||
assert_eq!(time_to_frame(TimeToFrameOptions { time: 1.26, fps: 10.0 }), 13.0);
|
||||
assert_eq!(frame_to_time(FrameToTimeOptions { frame: 13.0, fps: 10.0 }), 1.3);
|
||||
assert_eq!(snap_time_to_frame(SnapTimeToFrameOptions { time: 1.26, fps: 10.0 }), 1.3);
|
||||
assert_eq!(get_last_frame_time(GetLastFrameTimeOptions { duration: 10.0, fps: 5.0 }), 9.8);
|
||||
assert_eq!(
|
||||
get_snapped_seek_time(GetSnappedSeekTimeOptions {
|
||||
raw_time: 10.0,
|
||||
duration: 10.0,
|
||||
fps: 5.0,
|
||||
}),
|
||||
9.8,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "opencut-wasm"
|
||||
version = "0.1.2"
|
||||
edition = "2024"
|
||||
description = "Shared video editor logic compiled to WebAssembly"
|
||||
repository = "https://github.com/opencut/opencut"
|
||||
license = "MIT"
|
||||
|
||||
[lib]
|
||||
path = "src/wasm.rs"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
time = { version = "0.1.0", path = "../crates/time", features = ["wasm"] }
|
||||
@@ -0,0 +1,21 @@
|
||||
# opencut-wasm
|
||||
|
||||
Shared video editor logic compiled to WebAssembly. Used by the [OpenCut](https://github.com/opencut/opencut) web app.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
npm install opencut-wasm
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import { formatTimeCode } from "opencut-wasm";
|
||||
```
|
||||
|
||||
All exports are documented in the [TypeScript definitions](./opencut_wasm.d.ts).
|
||||
|
||||
## Source
|
||||
|
||||
Functions are implemented in Rust under [`rust/crates/`](../crates/). This package is the compiled WebAssembly output — do not edit it directly.
|
||||
@@ -0,0 +1 @@
|
||||
pub use time::*;
|
||||
Reference in New Issue
Block a user