diff --git a/Cargo.lock b/Cargo.lock index d0bc3077..abc0a223 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -133,6 +133,55 @@ dependencies = [ "winapi", ] +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +dependencies = [ + "anstyle", + "windows-sys 0.59.0", +] + [[package]] name = "anyhow" version = "1.0.86" @@ -511,7 +560,7 @@ dependencies = [ "bitflags 1.3.2", "cexpr 0.4.0", "clang-sys", - "clap", + "clap 2.34.0", "env_logger", "lazy_static", "lazycell", @@ -1086,12 +1135,72 @@ dependencies = [ "vec_map", ] +[[package]] +name = "clap" +version = "4.5.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim 0.11.1", +] + +[[package]] +name = "clap_derive" +version = "4.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + [[package]] name = "claxon" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4bfbf56724aa9eca8afa4fcfadeb479e722935bb2a0900c2d37e0cc477af0688" +[[package]] +name = "cli" +version = "0.1.0" +dependencies = [ + "cap-editor", + "cap-export", + "cap-ffmpeg-cli", + "cap-flags", + "cap-media", + "cap-project", + "cap-recording", + "cap-rendering", + "cap-utils", + "clap 4.5.23", + "editor", + "serde", + "serde_json", + "tokio", +] + [[package]] name = "clipboard-rs" version = "0.2.2" @@ -1207,6 +1316,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + [[package]] name = "com" version = "0.6.0" @@ -1586,6 +1701,31 @@ version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +[[package]] +name = "crossterm" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +dependencies = [ + "bitflags 2.6.0", + "crossterm_winapi", + "libc", + "mio 0.8.11", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "crunchy" version = "0.2.2" @@ -2030,6 +2170,17 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" +[[package]] +name = "editor" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3effaacc281d186774006894b787746c6870484cd08834ff42bfa2a6aa60ab6e" +dependencies = [ + "crossterm", + "derive_more", + "glam", +] + [[package]] name = "either" version = "1.13.0" @@ -2691,6 +2842,12 @@ dependencies = [ "xml-rs", ] +[[package]] +name = "glam" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "779ae4bf7e8421cf91c0b3b64e7e8b40b862fba4d393f59150042de7c4965a94" + [[package]] name = "glib" version = "0.18.5" @@ -3352,6 +3509,12 @@ dependencies = [ "once_cell", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itertools" version = "0.12.1" @@ -3864,6 +4027,18 @@ dependencies = [ "adler2", ] +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.48.0", +] + [[package]] name = "mio" version = "1.0.2" @@ -5992,9 +6167,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.214" +version = "1.0.216" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" +checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" dependencies = [ "serde_derive", ] @@ -6012,9 +6187,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.214" +version = "1.0.216" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" +checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" dependencies = [ "proc-macro2", "quote", @@ -6034,9 +6209,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.128" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" dependencies = [ "itoa 1.0.11", "memchr", @@ -6192,6 +6367,27 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +dependencies = [ + "libc", + "mio 0.8.11", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.2" @@ -7356,7 +7552,7 @@ dependencies = [ "backtrace", "bytes", "libc", - "mio", + "mio 1.0.2", "pin-project-lite", "signal-hook-registry", "socket2", @@ -7798,6 +7994,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.11.0" diff --git a/Cargo.toml b/Cargo.toml index d20ee260..4b076191 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = ["apps/desktop/src-tauri", "crates/*"] +members = [ "apps/cli","apps/desktop/src-tauri", "crates/*"] [workspace.dependencies] anyhow = "1.0.86" diff --git a/apps/cli/Cargo.toml b/apps/cli/Cargo.toml new file mode 100644 index 00000000..f7051142 --- /dev/null +++ b/apps/cli/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "cli" +version = "0.1.0" +edition = "2021" + +[dependencies] +clap = { version = "4.5.23", features = ["derive"] } +cap-utils = { path = "../../crates/utils" } +cap-project = { path = "../../crates/project" } +cap-rendering = { path = "../../crates/rendering" } +cap-ffmpeg-cli = { path = "../../crates/ffmpeg-cli" } +cap-editor = { path = "../../crates/editor" } +cap-media = { path = "../../crates/media" } +cap-flags = { path = "../../crates/flags" } +cap-recording = { path = "../../crates/recording" } +cap-export = { path = "../../crates/export" } +serde = "1.0.216" +serde_json = "1.0.133" +tokio.workspace = true +editor = "0.1.1" diff --git a/apps/cli/src/main.rs b/apps/cli/src/main.rs new file mode 100644 index 00000000..0aae8f2c --- /dev/null +++ b/apps/cli/src/main.rs @@ -0,0 +1,81 @@ +use std::{path::PathBuf, sync::Arc}; + +use cap_editor::create_segments; +use cap_project::{RecordingMeta, XY}; +use cap_rendering::RenderVideoConstants; +use clap::{Parser, Subcommand}; + +#[derive(Parser)] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + Export { + project_path: PathBuf, + output_path: Option, + }, +} + +#[tokio::main] +async fn main() { + let cli = Cli::parse(); + + match cli.command { + Commands::Export { + project_path, + output_path, + } => { + let project = serde_json::from_reader( + std::fs::File::open(project_path.join("project-config.json")).unwrap(), + ) + .unwrap(); + + let meta = RecordingMeta::load_for_project(&project_path).unwrap(); + let recordings = cap_rendering::ProjectRecordings::new(&meta); + + let render_options = cap_rendering::RenderOptions { + screen_size: XY::new( + recordings.segments[0].display.width, + recordings.segments[0].display.height, + ), + camera_size: recordings.segments[0] + .camera + .as_ref() + .map(|c| XY::new(c.width, c.height)), + }; + let render_constants = Arc::new( + RenderVideoConstants::new(render_options, &meta) + .await + .unwrap(), + ); + + let segments = create_segments(&meta); + + let project_output_path = project_path.join("output/result.mp4"); + let exporter = cap_export::Exporter::new( + project, + project_output_path.clone(), + |_| {}, + project_path.clone(), + meta, + render_constants, + &segments, + ) + .unwrap(); + + exporter.export_with_custom_muxer().await.unwrap(); + + let output_path = if let Some(output_path) = output_path { + std::fs::copy(&project_output_path, &output_path).unwrap(); + output_path + } else { + project_output_path + }; + + println!("Exported video to '{}'", output_path.display()); + } + } +} diff --git a/crates/editor/src/editor_instance.rs b/crates/editor/src/editor_instance.rs index 2dc6abe9..e45084ba 100644 --- a/crates/editor/src/editor_instance.rs +++ b/crates/editor/src/editor_instance.rs @@ -74,58 +74,7 @@ impl EditorInstance { .map(|c| XY::new(c.width, c.height)), }; - let segments = - match &meta.content { - cap_project::Content::SingleSegment { segment: s } => { - let audio = - Arc::new(s.audio.as_ref().map(|meta| { - AudioData::from_file(project_path.join(&meta.path)).unwrap() - })); - - let cursor = Arc::new(s.cursor_data(&meta).into()); - - let decoders = RecordingSegmentDecoders::new( - &meta, - SegmentVideoPaths { - display: s.display.path.as_path(), - camera: s.camera.as_ref().map(|c| c.path.as_path()), - }, - ); - - vec![Segment { - audio, - cursor, - decoders, - }] - } - cap_project::Content::MultipleSegments { inner } => { - let mut segments = vec![]; - - for s in &inner.segments { - let audio = Arc::new(s.audio.as_ref().map(|meta| { - AudioData::from_file(project_path.join(&meta.path)).unwrap() - })); - - let cursor = Arc::new(s.cursor_events(&meta)); - - let decoders = RecordingSegmentDecoders::new( - &meta, - SegmentVideoPaths { - display: s.display.path.as_path(), - camera: s.camera.as_ref().map(|c| c.path.as_path()), - }, - ); - - segments.push(Segment { - audio, - cursor, - decoders, - }); - } - - segments - } - }; + let segments = create_segments(&meta); let (frame_tx, frame_rx) = flume::bounded(4); @@ -324,80 +273,6 @@ impl Drop for EditorInstance { } } -// async fn create_frames_ws(frame_rx: mpsc::Receiver) -> (u16, mpsc::Sender<()>) { -// use axum::{ -// extract::{ -// ws::{Message, WebSocket, WebSocketUpgrade}, -// State, -// }, -// response::IntoResponse, -// routing::get, -// }; -// use tokio::sync::{mpsc::Receiver, Mutex}; - -// type RouterState = Arc>>; - -// async fn ws_handler( -// ws: WebSocketUpgrade, -// State(state): State, -// ) -> impl IntoResponse { -// // let rx = rx.lock().await.take().unwrap(); -// ws.on_upgrade(move |socket| handle_socket(socket, state)) -// } - -// async fn handle_socket(mut socket: WebSocket, state: RouterState) { -// let mut rx = state.lock().await; -// println!("socket connection established"); -// let now = std::time::Instant::now(); - -// loop { -// tokio::select! { -// _ = socket.recv() => { -// break; -// } -// msg = rx.recv() => { -// let Some(chunk) = msg else { -// continue; -// }; - -// match chunk { -// SocketMessage::Frame { width, height, mut data, stride } => { -// data.extend_from_slice(&stride.to_le_bytes()); -// data.extend_from_slice(&height.to_le_bytes()); -// data.extend_from_slice(&width.to_le_bytes()); - -// socket.send(Message::Binary(data)).await.unwrap(); -// } -// } -// } -// } -// } -// let elapsed = now.elapsed(); -// println!("Websocket closing after {elapsed:.2?}"); -// } - -// let router = axum::Router::new() -// .route(FRAMES_WS_PATH, get(ws_handler)) -// .with_state(Arc::new(Mutex::new(frame_rx))); - -// let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); -// let port = listener.local_addr().unwrap().port(); - -// let (shutdown_tx, mut shutdown_rx) = mpsc::channel::<()>(1); - -// tokio::spawn(async move { -// let server = axum::serve(listener, router.into_make_service()); -// tokio::select! { -// _ = server => {}, -// _ = shutdown_rx.recv() => { -// println!("WebSocket server shutting down"); -// } -// } -// }); - -// (port, shutdown_tx) -// } - type PreviewFrameInstruction = u32; pub struct EditorState { @@ -411,3 +286,56 @@ pub struct Segment { pub cursor: Arc, pub decoders: RecordingSegmentDecoders, } + +pub fn create_segments(meta: &RecordingMeta) -> Vec { + match &meta.content { + cap_project::Content::SingleSegment { segment: s } => { + let audio = Arc::new(s.audio.as_ref().map(|audio_meta| { + AudioData::from_file(meta.project_path.join(&audio_meta.path)).unwrap() + })); + + let cursor = Arc::new(s.cursor_data(&meta).into()); + + let decoders = RecordingSegmentDecoders::new( + &meta, + SegmentVideoPaths { + display: s.display.path.as_path(), + camera: s.camera.as_ref().map(|c| c.path.as_path()), + }, + ); + + vec![Segment { + audio, + cursor, + decoders, + }] + } + cap_project::Content::MultipleSegments { inner } => { + let mut segments = vec![]; + + for s in &inner.segments { + let audio = Arc::new(s.audio.as_ref().map(|audio_meta| { + AudioData::from_file(meta.project_path.join(&audio_meta.path)).unwrap() + })); + + let cursor = Arc::new(s.cursor_events(&meta)); + + let decoders = RecordingSegmentDecoders::new( + &meta, + SegmentVideoPaths { + display: s.display.path.as_path(), + camera: s.camera.as_ref().map(|c| c.path.as_path()), + }, + ); + + segments.push(Segment { + audio, + cursor, + decoders, + }); + } + + segments + } + } +} diff --git a/crates/editor/src/lib.rs b/crates/editor/src/lib.rs index a3b2cd76..61708db2 100644 --- a/crates/editor/src/lib.rs +++ b/crates/editor/src/lib.rs @@ -2,4 +2,4 @@ mod editor; mod editor_instance; mod playback; -pub use editor_instance::{EditorInstance, EditorState, Segment}; +pub use editor_instance::{create_segments, EditorInstance, EditorState, Segment}; diff --git a/crates/rendering/src/lib.rs b/crates/rendering/src/lib.rs index 146dd783..511c6887 100644 --- a/crates/rendering/src/lib.rs +++ b/crates/rendering/src/lib.rs @@ -27,9 +27,12 @@ use std::time::Instant; pub mod decoder; mod project_recordings; +mod zoom; pub use decoder::DecodedFrame; pub use project_recordings::{ProjectRecordings, SegmentRecordings}; +use zoom::*; + const STANDARD_CURSOR_HEIGHT: f32 = 75.0; #[derive(Debug, Clone, Copy, Type)] @@ -703,540 +706,6 @@ impl ProjectUniforms { } } -#[derive(Debug, PartialEq)] -pub struct ZoomKeyframe { - time: f64, - scale: f64, - position: ZoomPosition, - has_segment: bool, -} -#[derive(Debug, PartialEq, Clone, Copy)] -pub enum ZoomPosition { - Cursor, - Manual { x: f32, y: f32 }, -} -#[derive(Debug, PartialEq)] -pub struct ZoomKeyframes(Vec); - -pub const ZOOM_DURATION: f64 = 0.6; - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn single_keyframe() { - let segments = [ZoomSegment { - start: 0.5, - end: 1.5, - amount: 1.5, - mode: cap_project::ZoomMode::Manual { x: 0.2, y: 0.2 }, - }]; - - let keyframes = ZoomKeyframes::from_zoom_segments(&segments); - - pretty_assertions::assert_eq!( - keyframes, - ZoomKeyframes(vec![ - ZoomKeyframe { - time: 0.0, - scale: 1.0, - position: ZoomPosition::Manual { x: 0.0, y: 0.0 }, - has_segment: false, - }, - ZoomKeyframe { - time: 0.5, - scale: 1.0, - position: ZoomPosition::Manual { x: 0.2, y: 0.2 }, - has_segment: true, - }, - ZoomKeyframe { - time: 0.5 + ZOOM_DURATION, - scale: 1.5, - position: ZoomPosition::Manual { x: 0.2, y: 0.2 }, - has_segment: true, - }, - ZoomKeyframe { - time: 1.5, - scale: 1.5, - position: ZoomPosition::Manual { x: 0.2, y: 0.2 }, - has_segment: true, - }, - ZoomKeyframe { - time: 1.5 + ZOOM_DURATION, - scale: 1.0, - position: ZoomPosition::Manual { x: 0.2, y: 0.2 }, - has_segment: false, - } - ]) - ); - } - - #[test] - fn adjancent_different_position() { - let segments = [ - ZoomSegment { - start: 0.5, - end: 1.5, - amount: 1.5, - mode: cap_project::ZoomMode::Manual { x: 0.2, y: 0.2 }, - }, - ZoomSegment { - start: 1.5, - end: 2.5, - amount: 1.5, - mode: cap_project::ZoomMode::Manual { x: 0.8, y: 0.8 }, - }, - ]; - - let keyframes = ZoomKeyframes::from_zoom_segments(&segments); - - pretty_assertions::assert_eq!( - keyframes, - ZoomKeyframes(vec![ - ZoomKeyframe { - time: 0.0, - scale: 1.0, - position: ZoomPosition::Manual { x: 0.0, y: 0.0 }, - has_segment: false, - }, - ZoomKeyframe { - time: 0.5, - scale: 1.0, - position: ZoomPosition::Manual { x: 0.2, y: 0.2 }, - has_segment: true, - }, - ZoomKeyframe { - time: 0.5 + ZOOM_DURATION, - scale: 1.5, - position: ZoomPosition::Manual { x: 0.2, y: 0.2 }, - has_segment: true, - }, - ZoomKeyframe { - time: 1.5, - scale: 1.5, - position: ZoomPosition::Manual { x: 0.2, y: 0.2 }, - has_segment: true, - }, - ZoomKeyframe { - time: 1.5 + ZOOM_DURATION, - scale: 1.5, - position: ZoomPosition::Manual { x: 0.8, y: 0.8 }, - has_segment: true, - }, - ZoomKeyframe { - time: 2.5, - scale: 1.5, - position: ZoomPosition::Manual { x: 0.8, y: 0.8 }, - has_segment: true, - }, - ZoomKeyframe { - time: 2.5 + ZOOM_DURATION, - scale: 1.0, - position: ZoomPosition::Manual { x: 0.8, y: 0.8 }, - has_segment: false, - } - ]) - ); - } - - #[test] - fn adjacent_different_amount() { - let segments = [ - ZoomSegment { - start: 0.5, - end: 1.5, - amount: 1.5, - mode: cap_project::ZoomMode::Manual { x: 0.2, y: 0.2 }, - }, - ZoomSegment { - start: 1.5, - end: 2.5, - amount: 2.0, - mode: cap_project::ZoomMode::Manual { x: 0.2, y: 0.2 }, - }, - ]; - - let keyframes = ZoomKeyframes::from_zoom_segments(&segments); - - pretty_assertions::assert_eq!( - keyframes, - ZoomKeyframes(vec![ - ZoomKeyframe { - time: 0.0, - scale: 1.0, - position: ZoomPosition::Manual { x: 0.0, y: 0.0 }, - has_segment: false, - }, - ZoomKeyframe { - time: 0.5, - scale: 1.0, - position: ZoomPosition::Manual { x: 0.2, y: 0.2 }, - has_segment: true, - }, - ZoomKeyframe { - time: 0.5 + ZOOM_DURATION, - scale: 1.5, - position: ZoomPosition::Manual { x: 0.2, y: 0.2 }, - has_segment: true, - }, - ZoomKeyframe { - time: 1.5, - scale: 1.5, - position: ZoomPosition::Manual { x: 0.2, y: 0.2 }, - has_segment: true, - }, - ZoomKeyframe { - time: 1.5 + ZOOM_DURATION, - scale: 2.0, - position: ZoomPosition::Manual { x: 0.2, y: 0.2 }, - has_segment: true, - }, - ZoomKeyframe { - time: 2.5, - scale: 2.0, - position: ZoomPosition::Manual { x: 0.2, y: 0.2 }, - has_segment: true, - }, - ZoomKeyframe { - time: 2.5 + ZOOM_DURATION, - scale: 1.0, - position: ZoomPosition::Manual { x: 0.2, y: 0.2 }, - has_segment: false, - } - ]) - ); - } - - #[test] - fn gap() { - let segments = [ - ZoomSegment { - start: 0.5, - end: 1.5, - amount: 1.5, - mode: cap_project::ZoomMode::Manual { x: 0.0, y: 0.0 }, - }, - ZoomSegment { - start: 1.8, - end: 2.5, - amount: 1.5, - mode: cap_project::ZoomMode::Manual { x: 0.0, y: 0.0 }, - }, - ]; - - let keyframes = ZoomKeyframes::from_zoom_segments(&segments); - - let position = ZoomPosition::Manual { x: 0.0, y: 0.0 }; - let base = ZoomKeyframe { - time: 0.0, - scale: 1.0, - position, - has_segment: true, - }; - - pretty_assertions::assert_eq!( - keyframes, - ZoomKeyframes(vec![ - ZoomKeyframe { - has_segment: false, - ..base - }, - ZoomKeyframe { - time: 0.5, - scale: 1.0, - ..base - }, - ZoomKeyframe { - time: 0.5 + ZOOM_DURATION, - scale: 1.5, - ..base - }, - ZoomKeyframe { - time: 1.5, - scale: 1.5, - ..base - }, - ZoomKeyframe { - time: 1.8, - scale: 1.5 - (0.3 / ZOOM_DURATION) * 0.5, - has_segment: false, - ..base - }, - ZoomKeyframe { - time: 1.8 + ZOOM_DURATION, - scale: 1.5, - ..base - }, - ZoomKeyframe { - time: 2.5, - scale: 1.5, - ..base - }, - ZoomKeyframe { - time: 2.5 + ZOOM_DURATION, - scale: 1.0, - has_segment: false, - ..base - } - ]) - ); - } - - #[test] - fn project_config() { - let segments = [ - ZoomSegment { - start: 0.3966305848375451, - end: 1.396630584837545, - amount: 1.176, - mode: cap_project::ZoomMode::Manual { x: 0.0, y: 0.0 }, - }, - ZoomSegment { - start: 1.396630584837545, - end: 3.21881273465704, - amount: 1.204, - mode: cap_project::ZoomMode::Manual { x: 0.0, y: 0.0 }, - }, - ]; - - let keyframes = ZoomKeyframes::from_zoom_segments(&segments); - - let position = ZoomPosition::Manual { x: 0.0, y: 0.0 }; - let base = ZoomKeyframe { - time: 0.0, - scale: 1.0, - position, - has_segment: true, - }; - - pretty_assertions::assert_eq!( - keyframes, - ZoomKeyframes(vec![ - ZoomKeyframe { - has_segment: false, - ..base - }, - ZoomKeyframe { - time: 0.3966305848375451, - scale: 1.0, - ..base - }, - ZoomKeyframe { - time: 0.3966305848375451 + ZOOM_DURATION, - scale: 1.176, - ..base - }, - ZoomKeyframe { - time: 1.396630584837545, - scale: 1.176, - ..base - }, - ZoomKeyframe { - time: 1.396630584837545 + ZOOM_DURATION, - scale: 1.204, - ..base - }, - ZoomKeyframe { - time: 3.21881273465704, - scale: 1.204, - ..base - }, - ZoomKeyframe { - time: 3.21881273465704 + ZOOM_DURATION, - scale: 1.0, - has_segment: false, - ..base - }, - ]) - ); - } -} - -#[derive(Debug, PartialEq, Clone, Copy)] -pub struct InterpolatedZoom { - amount: f64, - t: f64, - position: ZoomPosition, -} - -impl ZoomKeyframes { - pub fn new(config: &ProjectConfiguration) -> Self { - let Some(zoom_segments) = config.timeline().map(|t| &t.zoom_segments) else { - return Self(vec![]); - }; - - Self::from_zoom_segments(zoom_segments) - } - - fn from_zoom_segments(segments: &[ZoomSegment]) -> Self { - if segments.is_empty() { - return Self(vec![]); - } - - let mut keyframes = vec![]; - - if segments[0].start != 0.0 { - keyframes.push(ZoomKeyframe { - time: 0.0, - scale: 1.0, - position: ZoomPosition::Manual { x: 0.0, y: 0.0 }, - has_segment: false, - }); - } - - for (i, segment) in segments.iter().enumerate() { - let position = match segment.mode { - cap_project::ZoomMode::Auto => ZoomPosition::Cursor, - cap_project::ZoomMode::Manual { x, y } => ZoomPosition::Manual { x, y }, - }; - - let prev = if i > 0 { segments.get(i - 1) } else { None }; - let next = segments.get(i + 1); - - if let Some(prev) = prev { - if prev.end + ZOOM_DURATION < segment.start { - // keyframes.push(ZoomKeyframe { - // time: segment.start, - // scale: 1.0, - // position, - // }); - } - } else { - if keyframes.len() != 0 { - keyframes.push(ZoomKeyframe { - time: segment.start, - scale: 1.0, - position, - has_segment: true, - }); - } - } - - keyframes.push(ZoomKeyframe { - time: segment.start + ZOOM_DURATION, - scale: segment.amount, - position, - has_segment: true, - }); - keyframes.push(ZoomKeyframe { - time: segment.end, - scale: segment.amount, - position, - has_segment: true, - }); - - if let Some(next) = next { - if segment.end + ZOOM_DURATION > next.start && next.start > segment.end { - let time = next.start - segment.end; - let t = time / ZOOM_DURATION; - - keyframes.push(ZoomKeyframe { - time: segment.end + time, - scale: 1.0 * t + (1.0 - t) * segment.amount, - position, - has_segment: false, - }); - } - } else { - keyframes.push(ZoomKeyframe { - time: segment.end + ZOOM_DURATION, - scale: 1.0, - position, - has_segment: false, - }); - } - } - - Self(dbg!(keyframes)) - } - - pub fn interpolate(&self, time: f64) -> InterpolatedZoom { - let default = InterpolatedZoom { - amount: 1.0, - position: ZoomPosition::Manual { x: 0.0, y: 0.0 }, - t: 0.0, - }; - - if !FLAGS.zoom { - return default; - } - - let prev_index = self - .0 - .iter() - .rev() - .position(|k| time >= k.time) - .map(|p| self.0.len() - 1 - p); - - let Some(prev_index) = prev_index else { - return default; - }; - - let next_index = prev_index + 1; - - let Some((prev, next)) = self.0.get(prev_index).zip(self.0.get(next_index)) else { - return default; - }; - - let keyframe_length = next.time - prev.time; - let delta_time = time - prev.time; - - let ease = if next.scale >= prev.scale { - bezier_easing::bezier_easing(0.1, 0.0, 0.3, 1.0).unwrap() - } else { - bezier_easing::bezier_easing(0.5, 0.0, 0.5, 1.0).unwrap() - }; - - let time_t = delta_time / keyframe_length; - - let keyframe_diff = next.scale - prev.scale; - - let amount = prev.scale + (keyframe_diff) * time_t; - - let time_t = ease(time_t as f32) as f64; - - // the process we use to get to this is way too convoluted lol - let t = ease( - (if prev.scale > 1.0 && next.scale > 1.0 { - if !next.has_segment { - (amount - 1.0) / (prev.scale - 1.0) - } else if !prev.has_segment { - (amount - 1.0) / (next.scale - 1.0) - } else { - 1.0 - } - } else if next.scale > 1.0 { - (amount - 1.0) / (next.scale - 1.0) - } else if prev.scale > 1.0 { - (amount - 1.0) / (prev.scale - 1.0) - } else { - 0.0 - }) as f32, - ) as f64; - - let position = match (&prev.position, &next.position) { - (ZoomPosition::Manual { x: x1, y: y1 }, ZoomPosition::Manual { x: x2, y: y2 }) => { - ZoomPosition::Manual { - x: x1 + (x2 - x1) * time_t as f32, - y: y1 + (y2 - y1) * time_t as f32, - } - } - _ => ZoomPosition::Manual { x: 0.0, y: 0.0 }, - }; - - InterpolatedZoom { - amount: if next.scale > prev.scale { - prev.scale + (next.scale - prev.scale) * t - } else { - prev.scale + (next.scale - prev.scale) * (1.0 - t) - }, - position, - t, - } - } -} - #[derive(Clone)] pub struct RenderedFrame { pub data: Vec, diff --git a/crates/rendering/src/zoom.rs b/crates/rendering/src/zoom.rs index e69de29b..0b95d701 100644 --- a/crates/rendering/src/zoom.rs +++ b/crates/rendering/src/zoom.rs @@ -0,0 +1,536 @@ +use cap_flags::FLAGS; +use cap_project::{ProjectConfiguration, ZoomSegment}; + +#[derive(Debug, PartialEq)] +pub struct ZoomKeyframe { + pub time: f64, + pub scale: f64, + pub position: ZoomPosition, + pub has_segment: bool, +} +#[derive(Debug, PartialEq, Clone, Copy)] +pub enum ZoomPosition { + Cursor, + Manual { x: f32, y: f32 }, +} +#[derive(Debug, PartialEq)] +pub struct ZoomKeyframes(Vec); + +pub const ZOOM_DURATION: f64 = 0.6; + +#[derive(Debug, PartialEq, Clone, Copy)] +pub struct InterpolatedZoom { + pub amount: f64, + pub t: f64, + pub position: ZoomPosition, +} + +impl ZoomKeyframes { + pub fn new(config: &ProjectConfiguration) -> Self { + let Some(zoom_segments) = config.timeline().map(|t| &t.zoom_segments) else { + return Self(vec![]); + }; + + Self::from_zoom_segments(zoom_segments) + } + + fn from_zoom_segments(segments: &[ZoomSegment]) -> Self { + if segments.is_empty() { + return Self(vec![]); + } + + let mut keyframes = vec![]; + + if segments[0].start != 0.0 { + keyframes.push(ZoomKeyframe { + time: 0.0, + scale: 1.0, + position: ZoomPosition::Manual { x: 0.0, y: 0.0 }, + has_segment: false, + }); + } + + for (i, segment) in segments.iter().enumerate() { + let position = match segment.mode { + cap_project::ZoomMode::Auto => ZoomPosition::Cursor, + cap_project::ZoomMode::Manual { x, y } => ZoomPosition::Manual { x, y }, + }; + + let prev = if i > 0 { segments.get(i - 1) } else { None }; + let next = segments.get(i + 1); + + if let Some(prev) = prev { + if prev.end + ZOOM_DURATION < segment.start { + // keyframes.push(ZoomKeyframe { + // time: segment.start, + // scale: 1.0, + // position, + // }); + } + } else { + if keyframes.len() != 0 { + keyframes.push(ZoomKeyframe { + time: segment.start, + scale: 1.0, + position, + has_segment: true, + }); + } + } + + keyframes.push(ZoomKeyframe { + time: segment.start + ZOOM_DURATION, + scale: segment.amount, + position, + has_segment: true, + }); + keyframes.push(ZoomKeyframe { + time: segment.end, + scale: segment.amount, + position, + has_segment: true, + }); + + if let Some(next) = next { + if segment.end + ZOOM_DURATION > next.start && next.start > segment.end { + let time = next.start - segment.end; + let t = time / ZOOM_DURATION; + + keyframes.push(ZoomKeyframe { + time: segment.end + time, + scale: 1.0 * t + (1.0 - t) * segment.amount, + position, + has_segment: false, + }); + } + } else { + keyframes.push(ZoomKeyframe { + time: segment.end + ZOOM_DURATION, + scale: 1.0, + position, + has_segment: false, + }); + } + } + + Self(dbg!(keyframes)) + } + + pub fn interpolate(&self, time: f64) -> InterpolatedZoom { + let default = InterpolatedZoom { + amount: 1.0, + position: ZoomPosition::Manual { x: 0.0, y: 0.0 }, + t: 0.0, + }; + + if !FLAGS.zoom { + return default; + } + + let prev_index = self + .0 + .iter() + .rev() + .position(|k| time >= k.time) + .map(|p| self.0.len() - 1 - p); + + let Some(prev_index) = prev_index else { + return default; + }; + + let next_index = prev_index + 1; + + let Some((prev, next)) = self.0.get(prev_index).zip(self.0.get(next_index)) else { + return default; + }; + + let keyframe_length = next.time - prev.time; + let delta_time = time - prev.time; + + let ease = if next.scale >= prev.scale { + bezier_easing::bezier_easing(0.1, 0.0, 0.3, 1.0).unwrap() + } else { + bezier_easing::bezier_easing(0.5, 0.0, 0.5, 1.0).unwrap() + }; + + let time_t = delta_time / keyframe_length; + + let keyframe_diff = next.scale - prev.scale; + + let amount = prev.scale + (keyframe_diff) * time_t; + + let time_t = ease(time_t as f32) as f64; + + // the process we use to get to this is way too convoluted lol + let t = ease( + (if prev.scale > 1.0 && next.scale > 1.0 { + if !next.has_segment { + (amount - 1.0) / (prev.scale - 1.0) + } else if !prev.has_segment { + (amount - 1.0) / (next.scale - 1.0) + } else { + 1.0 + } + } else if next.scale > 1.0 { + (amount - 1.0) / (next.scale - 1.0) + } else if prev.scale > 1.0 { + (amount - 1.0) / (prev.scale - 1.0) + } else { + 0.0 + }) as f32, + ) as f64; + + let position = match (&prev.position, &next.position) { + (ZoomPosition::Manual { x: x1, y: y1 }, ZoomPosition::Manual { x: x2, y: y2 }) => { + ZoomPosition::Manual { + x: x1 + (x2 - x1) * time_t as f32, + y: y1 + (y2 - y1) * time_t as f32, + } + } + _ => ZoomPosition::Manual { x: 0.0, y: 0.0 }, + }; + + InterpolatedZoom { + amount: if next.scale > prev.scale { + prev.scale + (next.scale - prev.scale) * t + } else { + prev.scale + (next.scale - prev.scale) * (1.0 - t) + }, + position, + t, + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn single_keyframe() { + let segments = [ZoomSegment { + start: 0.5, + end: 1.5, + amount: 1.5, + mode: cap_project::ZoomMode::Manual { x: 0.2, y: 0.2 }, + }]; + + let keyframes = ZoomKeyframes::from_zoom_segments(&segments); + + pretty_assertions::assert_eq!( + keyframes, + ZoomKeyframes(vec![ + ZoomKeyframe { + time: 0.0, + scale: 1.0, + position: ZoomPosition::Manual { x: 0.0, y: 0.0 }, + has_segment: false, + }, + ZoomKeyframe { + time: 0.5, + scale: 1.0, + position: ZoomPosition::Manual { x: 0.2, y: 0.2 }, + has_segment: true, + }, + ZoomKeyframe { + time: 0.5 + ZOOM_DURATION, + scale: 1.5, + position: ZoomPosition::Manual { x: 0.2, y: 0.2 }, + has_segment: true, + }, + ZoomKeyframe { + time: 1.5, + scale: 1.5, + position: ZoomPosition::Manual { x: 0.2, y: 0.2 }, + has_segment: true, + }, + ZoomKeyframe { + time: 1.5 + ZOOM_DURATION, + scale: 1.0, + position: ZoomPosition::Manual { x: 0.2, y: 0.2 }, + has_segment: false, + } + ]) + ); + } + + #[test] + fn adjancent_different_position() { + let segments = [ + ZoomSegment { + start: 0.5, + end: 1.5, + amount: 1.5, + mode: cap_project::ZoomMode::Manual { x: 0.2, y: 0.2 }, + }, + ZoomSegment { + start: 1.5, + end: 2.5, + amount: 1.5, + mode: cap_project::ZoomMode::Manual { x: 0.8, y: 0.8 }, + }, + ]; + + let keyframes = ZoomKeyframes::from_zoom_segments(&segments); + + pretty_assertions::assert_eq!( + keyframes, + ZoomKeyframes(vec![ + ZoomKeyframe { + time: 0.0, + scale: 1.0, + position: ZoomPosition::Manual { x: 0.0, y: 0.0 }, + has_segment: false, + }, + ZoomKeyframe { + time: 0.5, + scale: 1.0, + position: ZoomPosition::Manual { x: 0.2, y: 0.2 }, + has_segment: true, + }, + ZoomKeyframe { + time: 0.5 + ZOOM_DURATION, + scale: 1.5, + position: ZoomPosition::Manual { x: 0.2, y: 0.2 }, + has_segment: true, + }, + ZoomKeyframe { + time: 1.5, + scale: 1.5, + position: ZoomPosition::Manual { x: 0.2, y: 0.2 }, + has_segment: true, + }, + ZoomKeyframe { + time: 1.5 + ZOOM_DURATION, + scale: 1.5, + position: ZoomPosition::Manual { x: 0.8, y: 0.8 }, + has_segment: true, + }, + ZoomKeyframe { + time: 2.5, + scale: 1.5, + position: ZoomPosition::Manual { x: 0.8, y: 0.8 }, + has_segment: true, + }, + ZoomKeyframe { + time: 2.5 + ZOOM_DURATION, + scale: 1.0, + position: ZoomPosition::Manual { x: 0.8, y: 0.8 }, + has_segment: false, + } + ]) + ); + } + + #[test] + fn adjacent_different_amount() { + let segments = [ + ZoomSegment { + start: 0.5, + end: 1.5, + amount: 1.5, + mode: cap_project::ZoomMode::Manual { x: 0.2, y: 0.2 }, + }, + ZoomSegment { + start: 1.5, + end: 2.5, + amount: 2.0, + mode: cap_project::ZoomMode::Manual { x: 0.2, y: 0.2 }, + }, + ]; + + let keyframes = ZoomKeyframes::from_zoom_segments(&segments); + + pretty_assertions::assert_eq!( + keyframes, + ZoomKeyframes(vec![ + ZoomKeyframe { + time: 0.0, + scale: 1.0, + position: ZoomPosition::Manual { x: 0.0, y: 0.0 }, + has_segment: false, + }, + ZoomKeyframe { + time: 0.5, + scale: 1.0, + position: ZoomPosition::Manual { x: 0.2, y: 0.2 }, + has_segment: true, + }, + ZoomKeyframe { + time: 0.5 + ZOOM_DURATION, + scale: 1.5, + position: ZoomPosition::Manual { x: 0.2, y: 0.2 }, + has_segment: true, + }, + ZoomKeyframe { + time: 1.5, + scale: 1.5, + position: ZoomPosition::Manual { x: 0.2, y: 0.2 }, + has_segment: true, + }, + ZoomKeyframe { + time: 1.5 + ZOOM_DURATION, + scale: 2.0, + position: ZoomPosition::Manual { x: 0.2, y: 0.2 }, + has_segment: true, + }, + ZoomKeyframe { + time: 2.5, + scale: 2.0, + position: ZoomPosition::Manual { x: 0.2, y: 0.2 }, + has_segment: true, + }, + ZoomKeyframe { + time: 2.5 + ZOOM_DURATION, + scale: 1.0, + position: ZoomPosition::Manual { x: 0.2, y: 0.2 }, + has_segment: false, + } + ]) + ); + } + + #[test] + fn gap() { + let segments = [ + ZoomSegment { + start: 0.5, + end: 1.5, + amount: 1.5, + mode: cap_project::ZoomMode::Manual { x: 0.0, y: 0.0 }, + }, + ZoomSegment { + start: 1.8, + end: 2.5, + amount: 1.5, + mode: cap_project::ZoomMode::Manual { x: 0.0, y: 0.0 }, + }, + ]; + + let keyframes = ZoomKeyframes::from_zoom_segments(&segments); + + let position = ZoomPosition::Manual { x: 0.0, y: 0.0 }; + let base = ZoomKeyframe { + time: 0.0, + scale: 1.0, + position, + has_segment: true, + }; + + pretty_assertions::assert_eq!( + keyframes, + ZoomKeyframes(vec![ + ZoomKeyframe { + has_segment: false, + ..base + }, + ZoomKeyframe { + time: 0.5, + scale: 1.0, + ..base + }, + ZoomKeyframe { + time: 0.5 + ZOOM_DURATION, + scale: 1.5, + ..base + }, + ZoomKeyframe { + time: 1.5, + scale: 1.5, + ..base + }, + ZoomKeyframe { + time: 1.8, + scale: 1.5 - (0.3 / ZOOM_DURATION) * 0.5, + has_segment: false, + ..base + }, + ZoomKeyframe { + time: 1.8 + ZOOM_DURATION, + scale: 1.5, + ..base + }, + ZoomKeyframe { + time: 2.5, + scale: 1.5, + ..base + }, + ZoomKeyframe { + time: 2.5 + ZOOM_DURATION, + scale: 1.0, + has_segment: false, + ..base + } + ]) + ); + } + + #[test] + fn project_config() { + let segments = [ + ZoomSegment { + start: 0.3966305848375451, + end: 1.396630584837545, + amount: 1.176, + mode: cap_project::ZoomMode::Manual { x: 0.0, y: 0.0 }, + }, + ZoomSegment { + start: 1.396630584837545, + end: 3.21881273465704, + amount: 1.204, + mode: cap_project::ZoomMode::Manual { x: 0.0, y: 0.0 }, + }, + ]; + + let keyframes = ZoomKeyframes::from_zoom_segments(&segments); + + let position = ZoomPosition::Manual { x: 0.0, y: 0.0 }; + let base = ZoomKeyframe { + time: 0.0, + scale: 1.0, + position, + has_segment: true, + }; + + pretty_assertions::assert_eq!( + keyframes, + ZoomKeyframes(vec![ + ZoomKeyframe { + has_segment: false, + ..base + }, + ZoomKeyframe { + time: 0.3966305848375451, + scale: 1.0, + ..base + }, + ZoomKeyframe { + time: 0.3966305848375451 + ZOOM_DURATION, + scale: 1.176, + ..base + }, + ZoomKeyframe { + time: 1.396630584837545, + scale: 1.176, + ..base + }, + ZoomKeyframe { + time: 1.396630584837545 + ZOOM_DURATION, + scale: 1.204, + ..base + }, + ZoomKeyframe { + time: 3.21881273465704, + scale: 1.204, + ..base + }, + ZoomKeyframe { + time: 3.21881273465704 + ZOOM_DURATION, + scale: 1.0, + has_segment: false, + ..base + }, + ]) + ); + } +}