this repo has no description
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

✨ Add VGV encoding support

+172 -63
+1
.gitignore
··· 28 28 framedump.png 29 29 framedump.svg 30 30 results.csv 31 + examples/schedule-hell/schedule-hell.vgv
+34 -38
Cargo.lock
··· 1013 1013 checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04" 1014 1014 1015 1015 [[package]] 1016 + name = "diff-match-patch-rs" 1017 + version = "0.5.1" 1018 + source = "registry+https://github.com/rust-lang/crates.io-index" 1019 + checksum = "f1e836eb966678daed8789e698dbdb1016a260c3cdaca6172585e13580c4e4be" 1020 + dependencies = [ 1021 + "chrono", 1022 + "percent-encoding", 1023 + ] 1024 + 1025 + [[package]] 1016 1026 name = "digest" 1017 1027 version = "0.8.1" 1018 1028 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2625 2635 "js-sys", 2626 2636 "log", 2627 2637 "wasm-bindgen", 2628 - "windows-core 0.62.2", 2638 + "windows-core", 2629 2639 ] 2630 2640 2631 2641 [[package]] ··· 4590 4600 "toml 0.9.8", 4591 4601 "tungstenite", 4592 4602 "url", 4603 + "vgv", 4593 4604 "wasm-bindgen", 4594 4605 "watchexec", 4595 4606 "watchexec-events", ··· 5454 5465 checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 5455 5466 5456 5467 [[package]] 5468 + name = "vgv" 5469 + version = "0.1.0" 5470 + source = "git+https://github.com/gwennlbh/vgvf#fbde8ac4efde5f3c95548f82d6e09424800aea8a" 5471 + dependencies = [ 5472 + "anyhow", 5473 + "base64", 5474 + "diff-match-patch-rs", 5475 + "pico-args", 5476 + "resvg", 5477 + "serde", 5478 + "serde_json", 5479 + "tiny-skia", 5480 + "usvg", 5481 + ] 5482 + 5483 + [[package]] 5457 5484 name = "vst3-com" 5458 5485 version = "0.1.0" 5459 5486 source = "git+https://github.com/robbert-vdh/vst3-sys.git?branch=fix%2Fdrop-box-from-raw#b3ff4d775940f5b476b9d1cca02a90e07e1922a2" ··· 5726 5753 checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" 5727 5754 dependencies = [ 5728 5755 "windows-collections", 5729 - "windows-core 0.61.2", 5756 + "windows-core", 5730 5757 "windows-future", 5731 5758 "windows-link 0.1.3", 5732 5759 "windows-numerics", ··· 5738 5765 source = "registry+https://github.com/rust-lang/crates.io-index" 5739 5766 checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" 5740 5767 dependencies = [ 5741 - "windows-core 0.61.2", 5768 + "windows-core", 5742 5769 ] 5743 5770 5744 5771 [[package]] ··· 5750 5777 "windows-implement", 5751 5778 "windows-interface", 5752 5779 "windows-link 0.1.3", 5753 - "windows-result 0.3.4", 5754 - "windows-strings 0.4.2", 5755 - ] 5756 - 5757 - [[package]] 5758 - name = "windows-core" 5759 - version = "0.62.2" 5760 - source = "registry+https://github.com/rust-lang/crates.io-index" 5761 - checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" 5762 - dependencies = [ 5763 - "windows-implement", 5764 - "windows-interface", 5765 - "windows-link 0.2.1", 5766 - "windows-result 0.4.1", 5767 - "windows-strings 0.5.1", 5780 + "windows-result", 5781 + "windows-strings", 5768 5782 ] 5769 5783 5770 5784 [[package]] ··· 5773 5787 source = "registry+https://github.com/rust-lang/crates.io-index" 5774 5788 checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" 5775 5789 dependencies = [ 5776 - "windows-core 0.61.2", 5790 + "windows-core", 5777 5791 "windows-link 0.1.3", 5778 5792 "windows-threading", 5779 5793 ] ··· 5818 5832 source = "registry+https://github.com/rust-lang/crates.io-index" 5819 5833 checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" 5820 5834 dependencies = [ 5821 - "windows-core 0.61.2", 5835 + "windows-core", 5822 5836 "windows-link 0.1.3", 5823 5837 ] 5824 5838 ··· 5832 5846 ] 5833 5847 5834 5848 [[package]] 5835 - name = "windows-result" 5836 - version = "0.4.1" 5837 - source = "registry+https://github.com/rust-lang/crates.io-index" 5838 - checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" 5839 - dependencies = [ 5840 - "windows-link 0.2.1", 5841 - ] 5842 - 5843 - [[package]] 5844 5849 name = "windows-strings" 5845 5850 version = "0.4.2" 5846 5851 source = "registry+https://github.com/rust-lang/crates.io-index" 5847 5852 checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" 5848 5853 dependencies = [ 5849 5854 "windows-link 0.1.3", 5850 - ] 5851 - 5852 - [[package]] 5853 - name = "windows-strings" 5854 - version = "0.5.1" 5855 - source = "registry+https://github.com/rust-lang/crates.io-index" 5856 - checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" 5857 - dependencies = [ 5858 - "windows-link 0.2.1", 5859 5855 ] 5860 5856 5861 5857 [[package]]
+3 -2
Cargo.toml
··· 30 30 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 31 31 32 32 [features] 33 - default = ["cli", "vst", "mp4", "video-server"] 33 + default = ["cli", "vst", "video", "video-server"] 34 34 vst = [ 35 35 "cli", 36 36 "rand/thread_rng", ··· 48 48 "dep:miette", 49 49 ] 50 50 web = ["dep:wasm-bindgen", "dep:web-sys"] 51 - mp4 = ["dep:env_logger"] 51 + video = ["dep:env_logger", "dep:vgv"] 52 52 video-server = ["dep:axum"] 53 53 54 54 [dependencies] ··· 107 107 tungstenite = { version = "0.28.0", optional = true } 108 108 axum = { version = "0.8.6", optional = true, features = ["json"] } 109 109 quick-xml = "0.38.3" 110 + vgv = { git = "https://github.com/gwennlbh/vgvf", version = "0.1.0", optional = true} 110 111 111 112 112 113 [dev-dependencies]
+1 -1
examples/schedule-hell/Cargo.toml
··· 8 8 itertools = "0.14.0" 9 9 pico-args = { version = "0.5.0", features = ["combined-flags", "eq-separator"] } 10 10 rand = "0.9.0" 11 - shapemaker = { path = "../..", features = ["mp4"] } 11 + shapemaker = { path = "../..", features = ["video"] } 12 12 tokio = "1.48.0"
+6 -1
examples/schedule-hell/src/main.rs
··· 250 250 Ok(()) 251 251 }); 252 252 253 - if args.contains("--serve") { 253 + if args.contains("--vgv") { 254 + video.encode_to_vgv( 255 + args.free_from_str() 256 + .unwrap_or(String::from("schedule-hell.vgv")), 257 + ); 258 + } else if args.contains("--serve") { 254 259 video.serve("localhost:8000").await; 255 260 } else { 256 261 video.encode(
+2 -2
src/lib.rs
··· 4 4 let mut features = vec![]; 5 5 #[cfg(feature = "vst")] 6 6 features.push("vst"); 7 - #[cfg(feature = "mp4")] 8 - features.push("mp4"); 7 + #[cfg(feature = "video")] 8 + features.push("video"); 9 9 #[cfg(feature = "cli")] 10 10 features.push("cli"); 11 11 #[cfg(feature = "web")]
+5 -5
src/main.rs
··· 2 2 use shapemaker::*; 3 3 4 4 #[cfg(feature = "vst")] 5 - #[cfg(feature = "mp4")] 5 + #[cfg(feature = "video")] 6 6 use env_logger; 7 7 use measure_time::debug_time; 8 8 ··· 23 23 #[tokio::main] 24 24 pub async fn main() -> Result<()> { 25 25 #[cfg(feature = "vst")] 26 - #[cfg(feature = "mp4")] 26 + #[cfg(feature = "video")] 27 27 env_logger::init(); 28 28 run(cli::cli_args()).await 29 29 } ··· 89 89 Ok(probe.say("ping hehe")?) 90 90 } 91 91 92 - #[cfg(all(feature = "cli", not(feature = "mp4")))] 92 + #[cfg(all(feature = "cli", not(feature = "video")))] 93 93 fn run_video(_args: cli::Args) -> Result<()> { 94 94 println!( 95 - "Video rendering is disabled. Enable the mp4 feature to render videos." 95 + "Video rendering is disabled. Enable the video feature to render videos." 96 96 ); 97 97 Ok(()) 98 98 } 99 99 100 - #[cfg(all(feature = "cli", feature = "mp4"))] 100 + #[cfg(all(feature = "cli", feature = "video"))] 101 101 fn run_video(args: cli::Args) -> Result<()> { 102 102 use shapemaker::fonts::FontOptions; 103 103
+23 -2
src/rendering/svg.rs
··· 1 1 use std::{collections::HashMap, fmt::Display}; 2 2 3 3 use itertools::Itertools; 4 + use measure_time::debug_time; 4 5 5 6 use crate::{Color, ColorMapping, Point, Region}; 6 7 ··· 25 26 } 26 27 } 27 28 28 - pub fn tag(tag: &str) -> Element { 29 - Element::new(tag) 29 + pub fn tag(tag_name: &str) -> Element { 30 + Element::new(tag_name) 31 + } 32 + 33 + pub fn node(tag_name: &str) -> Node { 34 + tag(tag_name).node() 35 + } 36 + 37 + impl Node { 38 + pub fn is_empty(&self) -> bool { 39 + match self { 40 + Node::Element(e) => e.is_empty(), 41 + Node::Text(t) => t.is_empty(), 42 + Node::SVG(s) => s.is_empty(), 43 + } 44 + } 30 45 } 31 46 32 47 impl Element { ··· 148 163 children: vec![Node::Element(self)], 149 164 } 150 165 } 166 + 167 + pub fn is_empty(&self) -> bool { 168 + self.children.is_empty() 169 + } 151 170 } 152 171 153 172 pub enum PathInstruction { ··· 276 295 277 296 impl Display for Node { 278 297 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 298 + debug_time!("svg::Node::fmt"); 299 + 279 300 match self { 280 301 Node::Text(text) => write!(f, "{}", quick_xml::escape::escape(text)), 281 302 Node::SVG(svg) => write!(f, "{}", svg),
+86 -2
src/video/encoding.rs
··· 1 1 use super::{hooks::milliseconds_to_timestamp, Video}; 2 + use crate::rendering::svg; 2 3 use crate::Canvas; 3 4 use anyhow::Result; 5 + use itertools::Itertools; 4 6 use measure_time::debug_time; 5 7 use std::fs::File; 6 8 use std::io::Write; ··· 9 11 use std::{fs::create_dir_all, path::PathBuf}; 10 12 11 13 impl<AdditionalContext: Default> Video<AdditionalContext> { 14 + pub fn encode_to_vgv( 15 + &mut self, 16 + output_file: impl Into<PathBuf>, 17 + ) -> Result<()> { 18 + debug_time!("encode_to_vgv"); 19 + let output_file: PathBuf = output_file.into(); 20 + 21 + if output_file.exists() { 22 + std::fs::remove_file(&output_file)?; 23 + } 24 + 25 + create_dir_all( 26 + &output_file 27 + .parent() 28 + .expect("Given output file has no parent"), 29 + )?; 30 + 31 + let mut file = File::create(&output_file)?; 32 + 33 + self.progress_bar.set_position(0); 34 + self.progress_bar.set_prefix("Rendering"); 35 + self.progress_bar.set_message(""); 36 + 37 + self.initial_canvas.load_fonts()?; 38 + let initial_canvas = self.initial_canvas.clone(); 39 + let resolution = self.resolution; 40 + let (width, height) = initial_canvas.resolution_to_size_even(resolution); 41 + 42 + let (tx, rx) = 43 + std::sync::mpsc::sync_channel::<(Duration, svg::Node)>(1_000); 44 + 45 + let pb = self.progress_bar.clone(); 46 + 47 + let mut vgv_encoder = vgv::Encoder::new( 48 + vgv::InitializationParameters { 49 + w: width as _, 50 + h: height as _, 51 + d: (1000.0 / self.fps as f64) as _, 52 + bg: initial_canvas 53 + .background 54 + .unwrap_or_default() 55 + .render(&initial_canvas.colormap), 56 + }, 57 + format!( 58 + r#"width={w} height={h} viewBox="-{pad} -{pad} {w} {h}""#, 59 + w = initial_canvas.width(), 60 + h = initial_canvas.height(), 61 + pad = initial_canvas.canvas_outer_padding 62 + ), 63 + ); 64 + 65 + let vgv_thread = thread::spawn(move || { 66 + for (time, svg) in rx.iter() { 67 + if svg.is_empty() { 68 + break; 69 + } 70 + 71 + vgv_encoder.encode_svg(match svg { 72 + svg::Node::Text(text) => text, 73 + svg::Node::SVG(svg) => svg, 74 + svg::Node::Element(element) => element 75 + .children 76 + .iter() 77 + .map(|child| child.to_string()) 78 + .join(""), 79 + }); 80 + 81 + pb.set_position(time.as_millis() as _); 82 + pb.set_message(milliseconds_to_timestamp(time.as_millis() as _)); 83 + } 84 + 85 + vgv_encoder.dump(&mut file); 86 + }); 87 + 88 + self.render_all_frames(tx)?; 89 + 90 + vgv_thread.join().expect("VGV thread panicked"); 91 + 92 + Ok(()) 93 + } 94 + 12 95 fn setup_encoder( 13 96 &mut self, 14 97 output_path: impl Into<PathBuf>, ··· 75 158 let initial_canvas = self.initial_canvas.clone(); 76 159 let resolution = self.resolution; 77 160 78 - let (tx, rx) = std::sync::mpsc::sync_channel::<(Duration, String)>(1_000); 161 + let (tx, rx) = 162 + std::sync::mpsc::sync_channel::<(Duration, svg::Node)>(1_000); 79 163 80 164 let pb = self.progress_bar.clone(); 81 165 ··· 90 174 resolution, 91 175 time, 92 176 &initial_canvas, 93 - &svg, 177 + &svg.to_string(), 94 178 ) 95 179 .unwrap(); 96 180
+9 -8
src/video/engine.rs
··· 1 1 use super::{context::Context, hooks::milliseconds_to_timestamp, Video}; 2 - use crate::rendering::stringify_svg; 2 + use crate::rendering::svg; 3 3 use crate::{Canvas, SVGRenderable}; 4 4 use anyhow::Result; 5 5 use measure_time::debug_time; ··· 9 9 impl<AdditionalContext: Default> Video<AdditionalContext> { 10 10 pub fn render( 11 11 &self, 12 - output: SyncSender<(Duration, String)>, 12 + output: SyncSender<(Duration, svg::Node)>, 13 13 controller: impl Fn(&Context<AdditionalContext>) -> EngineControl, 14 14 ) -> Result<usize> { 15 15 debug_time!("render"); ··· 126 126 if !skip_rendering && context.frame != previous_rendered_frame { 127 127 output.send(( 128 128 Duration::from_millis(context.ms as _), 129 - stringify_svg(canvas.render_to_svg( 129 + canvas.render_to_svg( 130 130 canvas.colormap.clone(), 131 131 canvas.cell_size, 132 132 canvas.object_sizes, 133 133 "", 134 - )?), 134 + )?, 135 135 ))?; 136 136 137 137 rendered_frames_count += 1; ··· 149 149 } 150 150 } 151 151 152 - output.send((Duration::from_millis(context.ms as _), "".to_string()))?; 152 + output 153 + .send((Duration::from_millis(context.ms as _), svg::node("svg")))?; 153 154 154 155 println!("Rendered {rendered_frames_count} frames"); 155 156 Ok(rendered_frames_count) ··· 158 159 pub fn render_single_frame( 159 160 &self, 160 161 frame_no: usize, 161 - ) -> Result<(Duration, String)> { 162 + ) -> Result<(Duration, svg::Node)> { 162 163 debug_time!("render_single_frame"); 163 - let (tx, rx) = std::sync::mpsc::sync_channel::<(Duration, String)>(2); 164 + let (tx, rx) = std::sync::mpsc::sync_channel::<(Duration, svg::Node)>(2); 164 165 165 166 self.render(tx, |ctx| { 166 167 if ctx.frame == frame_no { ··· 188 189 189 190 pub fn render_all_frames( 190 191 &self, 191 - output: SyncSender<(Duration, String)>, 192 + output: SyncSender<(Duration, svg::Node)>, 192 193 ) -> Result<usize> { 193 194 self.render(output, |_| EngineControl::Render) 194 195 }
+1 -1
src/video/mod.rs
··· 3 3 pub mod engine; 4 4 pub mod hooks; 5 5 6 - #[cfg(feature = "mp4")] 6 + #[cfg(feature = "video")] 7 7 pub mod encoding; 8 8 9 9 #[cfg(feature = "video-server")]
+1 -1
src/video/server.rs
··· 26 26 println!("Frame number requested: {number}"); 27 27 28 28 match video.render_single_frame(number) { 29 - Ok((timecode, svg)) => svg.replace( 29 + Ok((timecode, svg)) => svg.to_string().replace( 30 30 "</svg>", 31 31 &format!(r#"<meta name="shapemaker:timecode" content="{timecode:?}" /></svg>"#) 32 32 ),