this repo has no description
3
fork

Configure Feed

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

♻️ Improve console UI, use results (wip), move schedule hell out of main.rs

authored by

Gwenn Le Bihan and committed by
Ewen Le Bihan
8dd66f70 564a7007

+381 -433
+62
Cargo.lock
··· 3 3 version = 3 4 4 5 5 [[package]] 6 + name = "addr2line" 7 + version = "0.21.0" 8 + source = "registry+https://github.com/rust-lang/crates.io-index" 9 + checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" 10 + dependencies = [ 11 + "gimli", 12 + ] 13 + 14 + [[package]] 15 + name = "adler" 16 + version = "1.0.2" 17 + source = "registry+https://github.com/rust-lang/crates.io-index" 18 + checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 + 20 + [[package]] 6 21 name = "aho-corasick" 7 22 version = "1.1.3" 8 23 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 43 58 version = "1.3.0" 44 59 source = "registry+https://github.com/rust-lang/crates.io-index" 45 60 checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" 61 + 62 + [[package]] 63 + name = "backtrace" 64 + version = "0.3.71" 65 + source = "registry+https://github.com/rust-lang/crates.io-index" 66 + checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" 67 + dependencies = [ 68 + "addr2line", 69 + "cc", 70 + "cfg-if", 71 + "libc", 72 + "miniz_oxide", 73 + "object", 74 + "rustc-demangle", 75 + ] 46 76 47 77 [[package]] 48 78 name = "block-buffer" ··· 221 251 ] 222 252 223 253 [[package]] 254 + name = "gimli" 255 + version = "0.28.1" 256 + source = "registry+https://github.com/rust-lang/crates.io-index" 257 + checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" 258 + 259 + [[package]] 224 260 name = "half" 225 261 version = "1.8.3" 226 262 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 355 391 ] 356 392 357 393 [[package]] 394 + name = "miniz_oxide" 395 + version = "0.7.2" 396 + source = "registry+https://github.com/rust-lang/crates.io-index" 397 + checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" 398 + dependencies = [ 399 + "adler", 400 + ] 401 + 402 + [[package]] 358 403 name = "nanoid" 359 404 version = "0.4.0" 360 405 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 379 424 checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" 380 425 381 426 [[package]] 427 + name = "object" 428 + version = "0.32.2" 429 + source = "registry+https://github.com/rust-lang/crates.io-index" 430 + checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" 431 + dependencies = [ 432 + "memchr", 433 + ] 434 + 435 + [[package]] 382 436 name = "once_cell" 383 437 version = "1.19.0" 384 438 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 545 599 checksum = "11707871ffa56ce568d4f15dd34c2f891a2aa5e4b3435b99b8f99938492525c3" 546 600 547 601 [[package]] 602 + name = "rustc-demangle" 603 + version = "0.1.23" 604 + source = "registry+https://github.com/rust-lang/crates.io-index" 605 + checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" 606 + 607 + [[package]] 548 608 name = "ryu" 549 609 version = "1.0.17" 550 610 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 607 667 version = "1.1.0" 608 668 dependencies = [ 609 669 "anyhow", 670 + "backtrace", 610 671 "chrono", 611 672 "chrono-human-duration", 673 + "console", 612 674 "docopt", 613 675 "getrandom", 614 676 "handlebars",
+2
Cargo.toml
··· 45 45 ] } 46 46 once_cell = "1.19.0" 47 47 nanoid = "0.4.0" 48 + console = { version = "0.15.8", features = ["windows-console-colors"] } 49 + backtrace = "0.3.71" 48 50 49 51 50 52 [dev-dependencies]
+2 -2
src/animation.rs
··· 1 1 use std::fmt::Display; 2 2 3 - use crate::{Canvas, Context, LaterHookCondition, RenderFunction}; 3 + use crate::Canvas; 4 4 5 5 /// Arguments: animation progress (from 0.0 to 1.0), canvas, current ms 6 - pub type AnimationUpdateFunction = dyn Fn(f32, &mut Canvas, usize); 6 + pub type AnimationUpdateFunction = dyn Fn(f32, &mut Canvas, usize) -> anyhow::Result<()>; 7 7 8 8 pub struct Animation { 9 9 pub name: String,
+1 -1
src/audio.rs
··· 78 78 79 79 impl Display for SyncData { 80 80 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 81 - write!(f, "SyncData @ {} bpm\n{} stems", self.bpm, self.stems.len()) 81 + write!(f, "SyncData @ {} bpm, {} stems", self.bpm, self.stems.len()) 82 82 } 83 83 } 84 84
+10 -5
src/canvas.rs
··· 1 1 use core::panic; 2 2 use std::{cmp, collections::HashMap, io::Write as _, ops::Range}; 3 3 4 + use anyhow::Result; 4 5 use itertools::Itertools as _; 5 6 use rand::Rng; 6 7 ··· 188 189 object_sizes: ObjectSizes::default(), 189 190 colormap: ColorMapping::default(), 190 191 layers: vec![], 191 - world_region: Region::new(0, 0, 3, 3), 192 + world_region: Region::new(0, 0, 3, 3).unwrap(), 192 193 background: None, 193 194 } 194 195 } ··· 379 380 } 380 381 381 382 pub fn random_point(&self, region: &Region) -> Point { 383 + region.ensure_nonempty().unwrap(); 382 384 Point( 383 385 rand::thread_rng().gen_range(region.x_range()), 384 386 rand::thread_rng().gen_range(region.y_range()), ··· 458 460 } 459 461 460 462 pub fn aspect_ratio(&self) -> f32 { 461 - return self.height() as f32 / self.width() as f32; 463 + return self.width() as f32 / self.height() as f32; 462 464 } 463 465 464 466 pub fn remove_all_objects_in(&mut self, region: &Region) { ··· 517 519 ) 518 520 } 519 521 520 - pub fn render(&mut self, layers: &Vec<&str>, render_background: bool) -> String { 522 + pub fn render(&mut self, layers: &Vec<&str>, render_background: bool) -> Result<String> { 521 523 let background_color = self.background.unwrap_or_default(); 522 524 let mut svg = svg::Document::new(); 523 525 if render_background { ··· 550 552 } 551 553 } 552 554 553 - svg.add(defs) 555 + let rendered = svg 556 + .add(defs) 554 557 .set( 555 558 "viewBox", 556 559 format!( ··· 562 565 ) 563 566 .set("width", self.width()) 564 567 .set("height", self.height()) 565 - .to_string() 568 + .to_string(); 569 + 570 + Ok(rendered) 566 571 } 567 572 }
+3 -3
src/examples.rs
··· 27 27 28 28 let draw_in = canvas.world_region.resized(-1, -1); 29 29 30 - let splines_area = Region::from_bottomleft(draw_in.bottomleft().translated(2, -1), (3, 3)); 31 - let red_circle_in = Region::from_topright(draw_in.topright().translated(-3, 0), (4, 3)); 30 + let splines_area = Region::from_bottomleft(draw_in.bottomleft().translated(2, -1), (3, 3)).unwrap(); 31 + let red_circle_in = Region::from_topright(draw_in.topright().translated(-3, 0), (4, 3)).unwrap(); 32 32 33 33 let red_circle_at = red_circle_in.random_point_within(); 34 34 ··· 113 113 114 114 pub fn title() -> Canvas { 115 115 let mut canvas = dna_analysis_machine(); 116 - let text_zone = Region::from_topleft(Point(8, 2), (3, 3)); 116 + let text_zone = Region::from_topleft(Point(8, 2), (3, 3)).unwrap(); 117 117 canvas.remove_all_objects_in(&text_zone); 118 118 let last_letter_at = &text_zone.bottomright().translated(1, 0); 119 119 canvas.remove_all_objects_in(&last_letter_at.region());
+13 -6
src/fill.rs
··· 1 - 2 1 use crate::{Color, ColorMapping, RenderCSS}; 3 2 4 3 #[derive(Debug, Clone, Copy)] ··· 17 16 TopDownDiagonal, 18 17 } 19 18 20 - const PATTERN_SIZE: usize = 8; 19 + impl HatchDirection {} 21 20 22 - impl HatchDirection {} 21 + impl Fill { 22 + pub fn opacify(&self, opacity: f32) -> Self { 23 + match self { 24 + Fill::Solid(color) => Fill::Translucent(*color, opacity), 25 + Fill::Translucent(color, _) => Fill::Translucent(*color, opacity), 26 + _ => self.clone(), 27 + } 28 + } 29 + } 23 30 24 31 impl RenderCSS for Fill { 25 32 fn render_fill_css(&self, colormap: &ColorMapping) -> String { ··· 74 81 pub fn pattern_id(&self) -> String { 75 82 if let Fill::Hatched(color, _, thickness, spacing) = self { 76 83 return format!( 77 - "pattern-{}-{}-{}", 84 + "pattern-{}-{}-{}-{}", 78 85 self.pattern_name(), 79 86 color.name(), 80 - thickness 87 + thickness, 88 + spacing 81 89 ); 82 90 } 83 91 String::from("") ··· 146 154 } 147 155 } 148 156 } 149 -
+10 -61
src/lib.rs
··· 1 + #![allow(uncommon_codepoints)] 2 + 1 3 pub mod animation; 2 4 pub mod audio; 3 5 pub mod canvas; ··· 13 15 pub mod preview; 14 16 pub mod region; 15 17 pub mod sync; 18 + pub mod ui; 16 19 pub mod video; 17 20 pub mod web; 18 21 pub use animation::*; 22 + use anyhow::Result; 19 23 pub use audio::*; 20 24 pub use canvas::*; 21 25 pub use color::*; ··· 30 34 pub use video::*; 31 35 pub use web::log; 32 36 33 - use indicatif::{ProgressBar, ProgressStyle}; 34 37 use nanoid::nanoid; 35 38 use std::fs::{self}; 36 - use std::ops::{Add, Div, Range, Sub}; 37 39 use std::path::PathBuf; 38 - use std::sync::{Arc, Mutex}; 39 - use std::thread::{self, JoinHandle}; 40 - use std::time; 41 40 use sync::SyncData; 42 41 43 - const PROGRESS_BARS_STYLE: &str = 44 - "{spinner:.cyan} {percent:03.bold.cyan}% {msg:<30} [{bar:100.bold.blue/dim.blue}] {eta:.cyan}"; 45 - 46 42 pub struct Context<'a, AdditionalContext = ()> { 47 43 pub frame: usize, 48 44 pub beat: usize, ··· 57 53 pub duration_override: Option<usize>, 58 54 } 59 55 60 - pub trait GetOrDefault { 61 - type Item; 62 - fn get_or(&self, index: usize, default: Self::Item) -> Self::Item; 63 - } 64 - 65 - impl<T: Copy> GetOrDefault for Vec<T> { 66 - type Item = T; 67 - fn get_or(&self, index: usize, default: T) -> T { 68 - *self.get(index).unwrap_or(&default) 69 - } 70 - } 71 - 72 56 impl<'a, C> Context<'a, C> { 73 57 pub fn stem(&self, name: &str) -> StemAtInstant { 74 58 let stems = &self.syncdata.stems; ··· 76 60 panic!("No stem named {:?} found.", name); 77 61 } 78 62 StemAtInstant { 79 - amplitude: stems[name].amplitude_db.get_or(self.ms, 0.0), 63 + amplitude: *stems[name].amplitude_db.get(self.ms).unwrap_or(&0.0), 80 64 amplitude_max: stems[name].amplitude_max, 81 65 velocity_max: stems[name] 82 66 .notes ··· 90 74 } 91 75 } 92 76 93 - pub fn dump_stems(&self, to: PathBuf) { 94 - std::fs::create_dir_all(&to); 77 + pub fn dump_stems(&self, to: PathBuf) -> Result<()> { 78 + std::fs::create_dir_all(&to)?; 95 79 for (name, stem) in self.syncdata.stems.iter() { 96 - fs::write(to.join(name), format!("{:?}", stem)); 80 + fs::write(to.join(name), format!("{:?}", stem))?; 97 81 } 82 + Ok(()) 98 83 } 99 84 100 85 pub fn marker(&self) -> String { ··· 185 170 } 186 171 } 187 172 188 - struct SpinState { 189 - pub spinner: ProgressBar, 190 - pub finished: Arc<Mutex<bool>>, 191 - pub thread: JoinHandle<()>, 192 - } 193 - 194 - impl SpinState { 195 - fn start(message: &str) -> Self { 196 - let spinner = ProgressBar::new(0).with_style( 197 - ProgressStyle::with_template(&("{spinner:.cyan} ".to_owned() + message)).unwrap(), 198 - ); 199 - spinner.tick(); 200 - 201 - let thread_spinner = spinner.clone(); 202 - let finished = Arc::new(Mutex::new(false)); 203 - let thread_finished = Arc::clone(&finished); 204 - let spinner_thread = thread::spawn(move || { 205 - while !*thread_finished.lock().unwrap() { 206 - thread_spinner.tick(); 207 - thread::sleep(time::Duration::from_millis(100)); 208 - } 209 - thread_spinner.finish_and_clear(); 210 - }); 211 - 212 - Self { 213 - spinner: spinner.clone(), 214 - finished, 215 - thread: spinner_thread, 216 - } 217 - } 218 - fn end(self, message: &str) { 219 - *self.finished.lock().unwrap() = true; 220 - self.thread.join().unwrap(); 221 - println!("{}", message); 222 - } 223 - } 224 - 173 + #[allow(unused)] 225 174 fn main() {}
+9 -240
src/main.rs
··· 1 + use anyhow::Result; 1 2 use itertools::Itertools; 2 3 use shapemaker::{ 3 4 cli::{canvas_from_cli, cli_args}, 4 5 *, 5 6 }; 6 7 7 - pub fn main() { 8 - run(cli_args()); 8 + pub fn main() -> Result<()> { 9 + run(cli_args()) 9 10 } 10 11 11 - pub fn run(args: cli::Args) { 12 + pub fn run(args: cli::Args) -> Result<()> { 12 13 let mut canvas = canvas_from_cli(&args); 13 14 14 15 if args.cmd_image && !args.cmd_video { 15 16 canvas = examples::dna_analysis_machine(); 16 - let rendered = canvas.render(&vec!["*"], true); 17 + let rendered = canvas.render(&vec!["*"], true)?; 17 18 if args.arg_file.ends_with(".svg") { 18 19 std::fs::write(args.arg_file, rendered).unwrap(); 19 20 } else { ··· 27 28 Err(e) => println!("Error saving image: {}", e), 28 29 } 29 30 } 30 - return; 31 + return Ok(()); 31 32 } 32 33 33 - let mut video = Video::<State>::new(canvas); 34 + let mut video = Video::<()>::new(canvas); 34 35 video.duration_override = args.flag_duration.map(|seconds| seconds * 1000); 35 36 video.start_rendering_at = args.flag_start.unwrap_or_default() * 1000; 36 37 video.fps = args.flag_fps.unwrap_or(30); 37 - video.audiofile = args.flag_audio.unwrap().into(); 38 - video = video 39 - .init(&|canvas: _, context: _| { 40 - context.extra = State { 41 - bass_pattern_at: Region::from_topleft(Point(6, 3), (3, 3)), 42 - first_kick_happened: false, 43 - }; 44 - canvas.set_background(Color::Black); 45 - 46 - let mut kicks = Layer::new("anchor kick"); 47 - 48 - let fill = Fill::Translucent(Color::White, 0.0); 49 - let circle_at = |x: usize, y: usize| Object::SmallCircle(Point(x, y)); 50 - 51 - let (end_x, end_y) = { 52 - let Point(x, y) = canvas.world_region.end; 53 - (x - 2, y - 2) 54 - }; 55 - kicks.set_object("top left", circle_at(1, 1).color(fill)); 56 - kicks.set_object("top right", circle_at(end_x, 1).color(fill)); 57 - kicks.set_object("bottom left", circle_at(1, end_y).color(fill)); 58 - kicks.set_object("bottom right", circle_at(end_x, end_y).color(fill)); 59 - canvas.add_or_replace_layer(kicks); 60 - 61 - let mut ch = Layer::new("ch"); 62 - ch.set_object("0", Object::Dot(Point(0, 0)).into()); 63 - canvas.add_or_replace_layer(ch); 64 - }) 65 - .sync_audio_with(&args.flag_sync_with.unwrap()) 66 - .on_note("anchor kick", &|canvas, ctx| { 67 - // ctx.extra.bass_pattern_at = region_cycle(&canvas.world_region, None); 68 - canvas 69 - .layer("anchor kick") 70 - .paint_all_objects(Fill::Translucent(Color::White, 1.0)); 71 - 72 - canvas.layer("anchor kick").flush(); 73 - 74 - // ctx.later_ms(200, &fade_out_kick_circles) 75 - ctx.animate(200, &|t, canvas, _| { 76 - canvas 77 - .layer("anchor kick") 78 - .paint_all_objects(Fill::Translucent(Color::White, 1.0 - t)); 79 - canvas.layer("anchor kick").flush(); 80 - }); 81 - }) 82 - .on_note("bass", &|canvas, ctx| { 83 - let mut new_layer = canvas.random_layer_within("bass", &ctx.extra.bass_pattern_at); 84 - new_layer.paint_all_objects(Fill::Solid(Color::White)); 85 - canvas.add_or_replace_layer(new_layer); 86 - }) 87 - .on_note("powerful clap hit, clap, perclap", &|canvas, ctx| { 88 - let mut new_layer = 89 - canvas.random_layer_within("claps", &ctx.extra.bass_pattern_at.translated(2, 0)); 90 - new_layer.paint_all_objects(Fill::Solid(Color::Red)); 91 - canvas.add_or_replace_layer(new_layer) 92 - }) 93 - .on_note( 94 - "rimshot, glitchy percs, hitting percs, glitchy percs", 95 - &|canvas, ctx| { 96 - let mut new_layer = canvas 97 - .random_layer_within("percs", &ctx.extra.bass_pattern_at.translated(2, 0)); 98 - new_layer.paint_all_objects(Fill::Translucent(Color::Red, 0.5)); 99 - canvas.add_or_replace_layer(new_layer); 100 - }, 101 - ) 102 - .on_note("qanda", &|canvas, ctx| { 103 - let mut new_layer = canvas.random_linelikes_within( 104 - "qanda", 105 - &ctx.extra.bass_pattern_at.translated(-1, -1).enlarged(1, 1), 106 - ); 107 - new_layer.paint_all_objects(Fill::Solid(Color::Orange)); 108 - new_layer.object_sizes.default_line_width = canvas.object_sizes.default_line_width 109 - * 4.0 110 - * ctx.stem("qanda").velocity_relative(); 111 - canvas.add_or_replace_layer(new_layer) 112 - }) 113 - .on_note("brokenup", &|canvas, ctx| { 114 - let mut new_layer = canvas 115 - .random_linelikes_within("brokenup", &ctx.extra.bass_pattern_at.translated(0, -2)); 116 - new_layer.paint_all_objects(Fill::Solid(Color::Yellow)); 117 - new_layer.object_sizes.default_line_width = canvas.object_sizes.default_line_width 118 - * 4.0 119 - * ctx.stem("brokenup").velocity_relative(); 120 - canvas.add_or_replace_layer(new_layer); 121 - }) 122 - .on_note("goup", &|canvas, ctx| { 123 - let mut new_layer = 124 - canvas.random_linelikes_within("goup", &ctx.extra.bass_pattern_at.translated(0, 2)); 125 - new_layer.paint_all_objects(Fill::Solid(Color::Green)); 126 - new_layer.object_sizes.default_line_width = 127 - canvas.object_sizes.default_line_width * 4.0 * ctx.stem("goup").velocity_relative(); 128 - canvas.add_or_replace_layer(new_layer); 129 - }) 130 - .on_note("ch", &|canvas, ctx| { 131 - let world = canvas.world_region.clone(); 132 - 133 - // keep only the last 2 dots 134 - let dots_to_keep = canvas 135 - .layer("ch") 136 - .objects 137 - .iter() 138 - .sorted_by_key(|(name, _)| name.parse::<usize>().unwrap()) 139 - .rev() 140 - .take(2) 141 - .map(|(name, _)| (name.clone())) 142 - .collect::<Vec<_>>(); 143 - 144 - let layer = canvas.layer("ch"); 145 - layer.object_sizes.empty_shape_stroke_width = 2.0; 146 - layer.objects.retain(|name, _| dots_to_keep.contains(name)); 147 - 148 - let object_name = format!("{}", ctx.ms); 149 - layer.set_object( 150 - &object_name, 151 - Object::Dot(world.resized(-1, -1).random_coordinates_within().into()) 152 - .color(Fill::Solid(Color::Cyan)), 153 - ); 154 - 155 - canvas.put_layer_on_top("ch"); 156 - canvas.layer("ch").flush(); 157 - }) 158 - .when_remaining(10, &|canvas, _| { 159 - canvas.root().set_object( 160 - "credits text", 161 - Object::RawSVG(Box::new(svg::node::Text::new("by ewen-lbh"))).into(), 162 - ); 163 - }) 164 - .command("remove", &|argumentsline, canvas, _| { 165 - let args = argumentsline.splitn(3, ' ').collect::<Vec<_>>(); 166 - canvas.remove_object(args[0]); 167 - }); 168 38 169 39 if args.flag_preview { 170 - video.preview_on(8888); 171 - } else { 172 - video.render_to(args.arg_file, args.flag_workers.unwrap_or(8), false); 173 - } 174 - } 175 - 176 - fn fade_out_kick_circles(canvas: &mut Canvas, _: usize) { 177 - canvas 178 - .layer("anchor kick") 179 - .paint_all_objects(Fill::Translucent(Color::White, 0.0)); 180 - 181 - canvas.layer("anchor kick").flush(); 182 - } 183 - 184 - fn update_stem_position( 185 - ctx: &mut shapemaker::Context<State>, 186 - canvas: &mut Canvas, 187 - layer_name: &str, 188 - offset: usize, 189 - ) { 190 - let (dx, dy) = ctx.extra.bass_pattern_at 191 - - region_cycle_with_offset( 192 - &canvas.world_region, 193 - Some(&ctx.extra.bass_pattern_at), 194 - offset, 195 - ); 196 - match canvas.layer_safe(layer_name) { 197 - Some(l) => l.move_all_objects(dx, dy), 198 - _ => (), 199 - } 200 - } 201 - 202 - #[derive(Default)] 203 - struct State { 204 - first_kick_happened: bool, 205 - bass_pattern_at: Region, 206 - } 207 - 208 - fn color_cycle(current_color: Color) -> Color { 209 - match current_color { 210 - Color::Blue => Color::Cyan, 211 - Color::Cyan => Color::Green, 212 - Color::Green => Color::Yellow, 213 - Color::Yellow => Color::Orange, 214 - Color::Orange => Color::Red, 215 - Color::Red => Color::Purple, 216 - Color::Purple => Color::Pink, 217 - Color::Pink => Color::White, 218 - Color::White => Color::Blue, 219 - _ => unreachable!(), 220 - } 221 - } 222 - 223 - fn region_cycle_with_offset(world: &Region, current: Option<&Region>, offset: usize) -> Region { 224 - if offset == 0 { 225 - return current.unwrap().clone(); 226 - } 227 - 228 - if offset == 1 { 229 - return region_cycle(world, current); 230 - } 231 - 232 - region_cycle_with_offset(world, current, offset - 1) 233 - } 234 - 235 - fn hat_region_cycle(world: &Region, current: &Region) -> (i32, i32) { 236 - let (end_x, end_y) = { 237 - let Point(x, y) = world.end; 238 - (x - 2, y - 2) 239 - }; 240 - 241 - match current.start { 242 - // top row 243 - Point(x, 1) if x < end_x => (1, 0), 244 - // right column 245 - Point(x, y) if x == end_x && y < end_y => (0, 1), 246 - // bottom row 247 - Point(x, y) if y == end_y && x > 1 => (-1, 0), 248 - // left column 249 - Point(1, y) if y > 1 => (0, -1), 250 - _ => unreachable!(), 251 - } 252 - } 253 - 254 - fn region_cycle(world: &Region, current: Option<&Region>) -> Region { 255 - let mut region = if let Some(current) = current { 256 - current.clone() 40 + video.preview_on(8888) 257 41 } else { 258 - Region::from_topleft(Point(1, 1), (2, 2)) 259 - }; 260 - 261 - let size = (region.width(), region.height()); 262 - // Move along x axis if possible 263 - if region.end.0 + size.0 <= world.end.0 - 1 { 264 - region.translate(size.0 as i32, 0) 42 + video.render_to(args.arg_file, args.flag_workers.unwrap_or(8), false) 265 43 } 266 - // Else go to x=0 and move along y axis 267 - else if region.end.1 + size.1 <= world.end.1 - 1 { 268 - region = Region::new(2, region.end.1, size.0 + 2, region.end.1 + size.1) 269 - } 270 - // Else go to origin 271 - else { 272 - region = Region::from_topleft(Point(1, 1), size) 273 - } 274 - region 275 44 }
+25 -11
src/midi.rs
··· 1 + use indicatif::ProgressBar; 1 2 use itertools::Itertools; 2 3 use midly::{MetaMessage, MidiMessage, TrackEvent, TrackEventKind}; 3 4 use std::{collections::HashMap, fmt::Debug, path::PathBuf}; ··· 18 19 } 19 20 } 20 21 21 - fn is_kick_channel(name: &str) -> bool { 22 - name.contains("kick") 23 - } 24 - 25 22 impl Syncable for MidiSynchronizer { 26 23 fn new(path: &str) -> Self { 27 24 Self { ··· 29 26 } 30 27 } 31 28 32 - fn load(&self) -> SyncData { 33 - let (now, notes_per_instrument) = load_notes(&self.midi_path); 29 + fn load(&self, progressbar: Option<&ProgressBar>) -> SyncData { 30 + let (now, notes_per_instrument) = load_notes(&self.midi_path, progressbar); 34 31 35 32 SyncData { 36 33 bpm: tempo_to_bpm(now.tempo), ··· 139 136 } 140 137 } 141 138 142 - fn load_notes<'a>(source: &PathBuf) -> (Now, HashMap<String, Vec<Note>>) { 139 + fn load_notes<'a>( 140 + source: &PathBuf, 141 + progressbar: Option<&ProgressBar>, 142 + ) -> (Now, HashMap<String, Vec<Note>>) { 143 143 // Read midi file using midly 144 + if let Some(pb) = progressbar { 145 + pb.set_length(1); 146 + pb.set_prefix("Loading"); 147 + pb.set_message("reading MIDI file"); 148 + pb.set_position(0); 149 + } 150 + 144 151 let raw = std::fs::read(source).unwrap(); 145 152 let midifile = midly::Smf::parse(&raw).unwrap(); 146 - println!("# of tracks\n\t{}", midifile.tracks.len()); 147 - println!("{:#?}", midifile.header); 148 153 149 154 let mut timeline = Timeline::new(); 150 155 let mut now = Now { ··· 156 161 }, 157 162 }; 158 163 164 + 159 165 // Get track names 160 166 let mut track_no = 0; 161 167 let mut track_names = HashMap::<usize, String>::new(); ··· 179 185 }, 180 186 ); 181 187 } 182 - 183 - println!("{:#?}", track_names); 184 188 185 189 // Convert ticks to absolute 186 190 let mut track_no = 0; ··· 215 219 absolute_tick_to_ms.insert(*tick, now.ms); 216 220 } 217 221 222 + if let Some(ref pb) = progressbar { 223 + pb.set_length(midifile.tracks.iter().map(|t| t.len()).sum::<usize>() as u64); 224 + pb.set_prefix("Loading"); 225 + pb.set_message("parsing MIDI events"); 226 + pb.set_position(0); 227 + } 228 + 218 229 // Add notes 219 230 let mut stem_notes = StemNotes::new(); 220 231 for (tick, tracks) in timeline.iter().sorted_by_key(|(tick, _)| *tick) { ··· 245 256 _ => {} 246 257 }, 247 258 _ => {} 259 + } 260 + if let Some(ref pb) = progressbar { 261 + pb.inc(1); 248 262 } 249 263 } 250 264 }
+4
src/objects.rs
··· 47 47 pub fn clear_filters(&mut self) { 48 48 self.2.clear(); 49 49 } 50 + 51 + pub fn fill(&self) -> Option<Fill> { 52 + self.1 53 + } 50 54 } 51 55 52 56 impl std::fmt::Display for ColoredObject {
+7 -4
src/preview.rs
··· 1 1 use std::{collections::HashMap, fs, path::PathBuf}; 2 2 3 + use anyhow::Result; 3 4 use handlebars::Handlebars; 4 5 use itertools::Itertools; 5 6 use serde_json::json; ··· 39 40 server_port: usize, 40 41 output_file: PathBuf, 41 42 audio_file: PathBuf, 42 - ) { 43 + ) -> Result<()> { 43 44 let first_frames = rendered_svg_frames 44 45 .iter() 45 46 // over 3000 loaded frames get really heavy on the browser (too much DOM nodes) ··· 49 50 .collect::<HashMap<usize, String>>(); 50 51 51 52 let contents = render_template(&first_frames, canvas, audio_file, server_port); 52 - fs::write(output_file, contents); 53 + fs::write(output_file, contents)?; 54 + Ok(()) 53 55 } 54 56 55 - pub fn start_preview_server(port: usize, frames: HashMap<usize, String>) { 57 + pub fn start_preview_server(port: usize, frames: HashMap<usize, String>) -> Result<()> { 56 58 let server = tiny_http::Server::http(format!("0.0.0.0:{}", port)).unwrap(); 57 59 println!("Preview server running on port {}", port); 58 60 let sorted_frames: Vec<(&usize, &String)> = ··· 84 86 field: "Access-Control-Allow-Origin".parse().unwrap(), 85 87 value: "*".parse().unwrap(), 86 88 }, 87 - )); 89 + ))?; 88 90 } 91 + Ok(()) 89 92 } 90 93 91 94 // returns (ms timestamp of first frame to send, number of frames to send)
+55 -26
src/region.rs
··· 1 1 use crate::Point; 2 + use anyhow::{format_err, Error, Result}; 3 + use backtrace::Backtrace; 2 4 use rand::Rng; 3 5 use wasm_bindgen::prelude::*; 4 6 ··· 35 37 return point; 36 38 } 37 39 } 40 + } 41 + 42 + pub fn ensure_nonempty(&self) -> Result<()> { 43 + if self.width() == 0 || self.height() == 0 { 44 + return Err(format_err!("Region {} is empty", self)); 45 + } 46 + 47 + Ok(()) 38 48 } 39 49 } 40 50 ··· 109 119 110 120 #[test] 111 121 fn test_sub_and_transate_coherence() { 112 - let a = Region::from_origin(Point(3, 3)); 122 + let a = Region::from_origin(Point(3, 3)).unwrap(); 113 123 let mut b = a.clone(); 114 124 b.translate(2, 3); 115 125 ··· 117 127 } 118 128 119 129 impl Region { 120 - pub fn new(start_x: usize, start_y: usize, end_x: usize, end_y: usize) -> Self { 130 + pub fn new(start_x: usize, start_y: usize, end_x: usize, end_y: usize) -> Result<Self, Error> { 121 131 let region = Self { 122 132 start: (start_x, start_y).into(), 123 133 end: (end_x, end_y).into(), 124 134 }; 125 - region.ensure_valid(); 126 - region 135 + region.ensure_valid() 136 + } 137 + 138 + pub fn from_points(start: Point, end: Point) -> Result<Self, Error> { 139 + Self::new(start.0, start.1, end.0, end.1) 127 140 } 128 141 129 142 pub fn bottomleft(&self) -> Point { ··· 157 170 ) 158 171 } 159 172 160 - pub fn from_origin(end: Point) -> Self { 173 + pub fn from_origin(end: Point) -> Result<Self> { 161 174 Self::new(0, 0, end.0, end.1) 162 175 } 163 176 164 - pub fn from_topleft(origin: Point, size: (usize, usize)) -> Self { 165 - Self::from(( 177 + pub fn from_topleft(origin: Point, size: (usize, usize)) -> Result<Self> { 178 + Self::from_points( 166 179 origin, 167 180 origin.translated_by(Point::from(size).translated(-1, -1)), 168 - )) 181 + ) 169 182 } 170 183 171 - pub fn from_bottomleft(origin: Point, size: (usize, usize)) -> Self { 184 + pub fn from_bottomleft(origin: Point, size: (usize, usize)) -> Result<Self> { 172 185 Self::from_topleft(origin.translated(0, -(size.1 as i32 - 1)), size) 173 186 } 174 187 175 - pub fn from_bottomright(origin: Point, size: (usize, usize)) -> Self { 176 - Self::from(( 188 + pub fn from_bottomright(origin: Point, size: (usize, usize)) -> Result<Self> { 189 + Self::from_points( 177 190 origin.translated_by(Point::from(size).translated(-1, -1)), 178 191 origin, 179 - )) 192 + ) 180 193 } 181 194 182 - pub fn from_topright(origin: Point, size: (usize, usize)) -> Self { 195 + pub fn from_topright(origin: Point, size: (usize, usize)) -> Result<Self> { 183 196 Self::from_topleft(origin.translated(-(size.0 as i32 - 1), 0), size) 184 197 } 185 198 186 - pub fn from_center_and_size(center: Point, size: (usize, usize)) -> Self { 199 + pub fn from_center_and_size(center: Point, size: (usize, usize)) -> Result<Self> { 187 200 let half_size = (size.0 / 2, size.1 / 2); 188 201 Self::new( 189 202 center.0 - half_size.0, ··· 194 207 } 195 208 196 209 // panics if the region is invalid 197 - pub fn ensure_valid(self) -> Self { 210 + pub fn ensure_valid(self) -> Result<Self> { 198 211 if self.start.0 >= self.end.0 || self.start.1 >= self.end.1 { 199 - panic!( 212 + return Err(format_err!( 200 213 "Invalid region: start ({:?}) >= end ({:?})", 201 - self.start, self.end 202 - ) 214 + self.start, 215 + self.end 216 + )); 203 217 } 204 - self 218 + 219 + Ok(self) 205 220 } 206 221 207 222 pub fn translate(&mut self, dx: i32, dy: i32) { ··· 211 226 pub fn translated(&self, dx: i32, dy: i32) -> Self { 212 227 Self { 213 228 start: ( 214 - (self.start.0 as i32 + dx) as usize, 215 - (self.start.1 as i32 + dy) as usize, 229 + (self.start.0 as i32 + dx).max(0) as usize, 230 + (self.start.1 as i32 + dy).max(0) as usize, 216 231 ) 217 232 .into(), 218 233 end: ( 219 - (self.end.0 as i32 + dx) as usize, 220 - (self.end.1 as i32 + dy) as usize, 234 + (self.end.0 as i32 + dx).max(0) as usize, 235 + (self.end.1 as i32 + dy).max(0) as usize, 221 236 ) 222 237 .into(), 223 238 } 224 - .ensure_valid() 225 239 } 226 240 227 241 /// adds dx and dy to the end of the region (dx and dy are _not_ multiplicative but **additive** factors) 228 242 pub fn enlarged(&self, dx: i32, dy: i32) -> Self { 229 - Self { 243 + let resulting = Self { 230 244 start: self.start, 231 245 end: ( 232 246 (self.end.0 as i32 + dx) as usize, 233 247 (self.end.1 as i32 + dy) as usize, 234 248 ) 235 249 .into(), 250 + }; 251 + 252 + if resulting.ensure_valid().is_err() { 253 + let bt = Backtrace::new(); 254 + println!("WARN: Did not enlarge region {self} with ({dx}, {dy}), it would result in a non-valid region\n{bt:?}"); 255 + return *self; 236 256 } 237 - .ensure_valid() 257 + 258 + resulting 238 259 } 239 260 240 261 /// resized is like enlarged, but transforms from the center, by first translating the region by (-dx, -dy) ··· 276 297 } 277 298 278 299 pub fn width(&self) -> usize { 300 + if self.end.0 < self.start.0 { 301 + return 0; 302 + } 303 + 279 304 self.end.0 - self.start.0 + 1 280 305 } 281 306 282 307 pub fn height(&self) -> usize { 308 + if self.end.1 < self.start.1 { 309 + return 0; 310 + } 311 + 283 312 self.end.1 - self.start.1 + 1 284 313 } 285 314
+1 -1
src/sync.rs
··· 5 5 pub type TimestampMS = usize; 6 6 7 7 pub trait Syncable { 8 - fn load(&self) -> SyncData; 9 8 fn new(path: &str) -> Self; 9 + fn load(&self, progress: Option<&indicatif::ProgressBar>) -> SyncData; 10 10 } 11 11 12 12 #[derive(Debug, Default)]
+71
src/ui.rs
··· 1 + use console::Style; 2 + use indicatif::{ProgressBar, ProgressStyle}; 3 + use std::sync::{Arc, Mutex}; 4 + use std::thread::{self, JoinHandle}; 5 + use std::time; 6 + 7 + pub const PROGRESS_BARS_STYLE: &str = 8 + "{prefix:>12.bold.cyan} [{bar:40}] {pos}/{len}: {msg} ({eta} left)"; 9 + 10 + pub struct Spinner { 11 + pub spinner: ProgressBar, 12 + pub finished: Arc<Mutex<bool>>, 13 + pub thread: JoinHandle<()>, 14 + } 15 + 16 + impl Spinner { 17 + pub fn start(message: &str) -> Self { 18 + let spinner = ProgressBar::new(0).with_style( 19 + ProgressStyle::with_template(&("{spinner:.cyan} ".to_owned() + message)).unwrap(), 20 + ); 21 + spinner.tick(); 22 + 23 + let thread_spinner = spinner.clone(); 24 + let finished = Arc::new(Mutex::new(false)); 25 + let thread_finished = Arc::clone(&finished); 26 + let spinner_thread = thread::spawn(move || { 27 + while !*thread_finished.lock().unwrap() { 28 + thread_spinner.tick(); 29 + thread::sleep(time::Duration::from_millis(100)); 30 + } 31 + thread_spinner.finish_and_clear(); 32 + }); 33 + 34 + Self { 35 + spinner: spinner.clone(), 36 + finished, 37 + thread: spinner_thread, 38 + } 39 + } 40 + 41 + pub fn end(self, message: &str) { 42 + *self.finished.lock().unwrap() = true; 43 + self.thread.join().unwrap(); 44 + println!("{}", message); 45 + } 46 + } 47 + 48 + pub fn setup_progress_bar(total: u64, verb: &'static str) -> ProgressBar { 49 + indicatif::ProgressBar::new(total) 50 + .with_prefix(verb) 51 + .with_style( 52 + indicatif::ProgressStyle::with_template(PROGRESS_BARS_STYLE) 53 + .unwrap() 54 + .progress_chars("=> "), 55 + ) 56 + } 57 + 58 + pub trait Log { 59 + fn log(&self, verb: &'static str, message: &str); 60 + } 61 + 62 + pub fn format_log_msg(verb: &'static str, message: &str) -> String { 63 + let style = Style::new().bold().cyan(); 64 + format!("{}: {}", style.apply_to(format!("{verb:>12}")), message) 65 + } 66 + 67 + impl Log for ProgressBar { 68 + fn log(&self, verb: &'static str, message: &str) { 69 + self.println(format_log_msg(verb, message)); 70 + } 71 + }
+88 -55
src/video.rs
··· 3 3 collections::HashMap, 4 4 fmt::Formatter, 5 5 fs::{create_dir, create_dir_all, remove_dir_all}, 6 + panic, 6 7 path::{Path, PathBuf}, 7 8 sync::Arc, 8 - thread, 9 9 }; 10 10 11 + use std::thread; 12 + 13 + use anyhow::Result; 11 14 use chrono::{DateTime, NaiveDateTime}; 12 - use indicatif::ProgressBar; 15 + use indicatif::{ProgressBar, ProgressIterator}; 13 16 14 17 use crate::{ 15 - preview, sync::SyncData, Canvas, Context, Fill, MidiSynchronizer, MusicalDurationUnit, Object, 16 - SpinState, Syncable, PROGRESS_BARS_STYLE, 18 + preview, 19 + sync::SyncData, 20 + ui::{self, setup_progress_bar, Log as _}, 21 + Canvas, ColoredObject, Context, MidiSynchronizer, MusicalDurationUnit, Syncable, 17 22 }; 18 23 19 - pub type RenderFunction<C> = dyn Fn(&mut Canvas, &mut Context<C>); 20 - pub type CommandAction<C> = dyn Fn(String, &mut Canvas, &mut Context<C>); 24 + pub type BeatNumber = usize; 25 + pub type FrameNumber = usize; 26 + pub type Millisecond = usize; 27 + 28 + pub type RenderFunction<C> = dyn Fn(&mut Canvas, &mut Context<C>) -> anyhow::Result<()>; 29 + pub type CommandAction<C> = dyn Fn(String, &mut Canvas, &mut Context<C>) -> anyhow::Result<()>; 21 30 22 31 /// Arguments: canvas, context, previous rendered beat, previous rendered frame 23 - pub type HookCondition<C> = dyn Fn(&Canvas, &Context<C>, usize, usize) -> bool; 32 + pub type HookCondition<C> = dyn Fn(&Canvas, &Context<C>, BeatNumber, FrameNumber) -> bool; 24 33 25 34 /// Arguments: canvas, context, current milliseconds timestamp 26 - pub type LaterRenderFunction = dyn Fn(&mut Canvas, usize); 35 + pub type LaterRenderFunction = dyn Fn(&mut Canvas, Millisecond) -> anyhow::Result<()>; 27 36 28 37 /// Arguments: canvas, context, previous rendered beat 29 - pub type LaterHookCondition<C> = dyn Fn(&Canvas, &Context<C>, usize) -> bool; 38 + pub type LaterHookCondition<C> = dyn Fn(&Canvas, &Context<C>, BeatNumber) -> bool; 30 39 31 40 #[derive(Debug)] 32 41 pub struct Video<C> { ··· 41 50 pub resolution: usize, 42 51 pub duration_override: Option<usize>, 43 52 pub start_rendering_at: usize, 53 + pub progress_bar: indicatif::ProgressBar, 44 54 } 45 55 pub struct Hook<C> { 46 56 pub when: Box<HookCondition<C>>, ··· 97 107 audiofile: PathBuf::new(), 98 108 duration_override: None, 99 109 start_rendering_at: 0, 110 + progress_bar: setup_progress_bar(0, ""), 100 111 } 101 112 } 102 113 103 114 pub fn sync_audio_with(self, sync_data_path: &str) -> Self { 104 115 if sync_data_path.ends_with(".mid") || sync_data_path.ends_with(".midi") { 105 116 let loader = MidiSynchronizer::new(sync_data_path); 106 - let syncdata = loader.load(); 107 - println!("Loaded MIDI sync data: {}", syncdata); 117 + let syncdata = loader.load(Some(&self.progress_bar)); 118 + self.progress_bar.finish(); 108 119 return Self { syncdata, ..self }; 109 120 } 110 121 111 122 panic!("Unsupported sync data format"); 112 123 } 113 124 114 - pub fn build_video(&self, render_to: &str) -> Result<(), String> { 125 + pub fn build_video(&self, render_to: &str) -> Result<()> { 115 126 let mut command = std::process::Command::new("ffmpeg"); 116 127 117 128 command ··· 140 151 println!("Running command: {:?}", command); 141 152 142 153 match command.output() { 143 - Err(e) => Err(format!("Failed to execute ffmpeg: {}", e)), 154 + Err(e) => Err(anyhow::format_err!("Failed to execute ffmpeg: {}", e).into()), 144 155 Ok(r) => { 145 156 println!("{}", std::str::from_utf8(&r.stdout).unwrap()); 146 157 println!("{}", std::str::from_utf8(&r.stderr).unwrap()); ··· 316 327 create_object: &'static dyn Fn( 317 328 &Canvas, 318 329 &mut Context<AdditionalContext>, 319 - ) -> (Object, Option<Fill>), 330 + ) -> Result<ColoredObject>, 320 331 ) -> Self { 321 332 self.with_hook(Hook { 322 333 when: Box::new(move |_, ctx, _, _| { ··· 325 336 .any(|stem_name| ctx.stem(stem_name).notes.iter().any(|note| note.is_on())) 326 337 }), 327 338 render_function: Box::new(move |canvas, ctx| { 328 - let (object, fill) = create_object(canvas, ctx); 329 - canvas.add_object(layer_name, object_name, object, fill); 339 + let object = create_object(canvas, ctx)?; 340 + canvas.layer(&layer_name).set_object(object_name, object); 341 + Ok(()) 330 342 }), 331 343 }) 332 344 .with_hook(Hook { ··· 336 348 || ctx.stem(stem_name).notes.iter().any(|note| note.is_off()) 337 349 }) 338 350 }), 339 - render_function: Box::new(move |canvas, _| canvas.remove_object(object_name)), 351 + render_function: Box::new(move |canvas, _| { 352 + canvas.remove_object(object_name); 353 + Ok(()) 354 + }), 340 355 }) 341 356 } 342 357 ··· 451 466 .unwrap() 452 467 } 453 468 454 - pub fn preview_on(&self, port: usize) { 469 + pub fn preview_on(&self, port: usize) -> Result<()> { 455 470 let mut rendered_frames: HashMap<usize, String> = HashMap::new(); 456 471 let progress_bar = self.setup_progress_bar(); 457 472 458 - for (frame, _, ms) in self.render_frames(&progress_bar, vec!["*"], true) { 473 + for (frame, _, ms) in self.render_frames(&progress_bar, vec!["*"], true)? { 459 474 rendered_frames.insert(ms, frame); 460 475 } 461 476 ··· 467 482 port, 468 483 PathBuf::from(".").join("preview.html"), 469 484 self.audiofile.clone(), 470 - ); 471 - preview::start_preview_server(port, rendered_frames); 485 + )?; 486 + 487 + preview::start_preview_server(port, rendered_frames) 472 488 } 473 489 474 - pub fn render_to(&self, output_file: String, workers_count: usize, preview_only: bool) -> () { 475 - self.render_composition(output_file, vec!["*"], true, workers_count, preview_only); 490 + pub fn render_to( 491 + &self, 492 + output_file: String, 493 + workers_count: usize, 494 + preview_only: bool, 495 + ) -> Result<()> { 496 + self.render_composition(output_file, vec!["*"], true, workers_count, preview_only) 476 497 } 477 498 478 - pub fn render_layers_in(&self, output_directory: String, workers_count: usize) -> () { 499 + pub fn render_layers_in(&self, output_directory: String, workers_count: usize) -> Result<()> { 479 500 for composition in self 480 501 .initial_canvas 481 502 .layers ··· 488 509 false, 489 510 workers_count, 490 511 false, 491 - ); 512 + )?; 492 513 } 514 + Ok(()) 493 515 } 494 516 495 517 // Returns a triple of (SVG content, frame number, millisecond at frame) ··· 498 520 progress_bar: &ProgressBar, 499 521 composition: Vec<&str>, 500 522 render_background: bool, 501 - ) -> Vec<(String, usize, usize)> { 523 + ) -> Result<Vec<(String, usize, usize)>> { 502 524 let mut context = Context { 503 525 frame: 0, 504 526 beat: 0, ··· 519 541 let mut previous_rendered_frame = 0; 520 542 let mut frames_to_write: Vec<(String, usize, usize)> = vec![]; 521 543 522 - for _ in 0..self.duration_ms() + self.start_rendering_at { 544 + let render_ms_range = 0..self.duration_ms() + self.start_rendering_at; 545 + 546 + self.progress_bar.set_length(render_ms_range.len() as u64); 547 + 548 + for _ in render_ms_range 549 + .into_iter() 550 + .progress_with(self.progress_bar.clone()) 551 + { 523 552 context.ms += 1_usize; 524 553 context.timestamp = milliseconds_to_timestamp(context.ms).to_string(); 525 554 context.beat_fractional = (context.bpm * context.ms) as f32 / (1000.0 * 60.0); 526 555 context.beat = context.beat_fractional as usize; 527 556 context.frame = ((self.fps * context.ms) as f64 / 1000.0) as usize; 557 + 558 + progress_bar.set_message(context.timestamp.clone()); 528 559 529 560 if context.marker() != "" { 530 561 progress_bar.println(format!( ··· 556 587 previous_rendered_beat, 557 588 previous_rendered_frame, 558 589 ) { 559 - (hook.render_function)(&mut canvas, &mut context); 590 + (hook.render_function)(&mut canvas, &mut context)?; 560 591 } 561 592 } 562 593 ··· 564 595 565 596 for (i, hook) in context.later_hooks.iter().enumerate() { 566 597 if (hook.when)(&canvas, &context, previous_rendered_beat) { 567 - (hook.render_function)(&mut canvas, context.ms); 598 + (hook.render_function)(&mut canvas, context.ms)?; 568 599 if hook.once { 569 600 later_hooks_to_delete.push(i); 570 601 } ··· 580 611 } 581 612 582 613 if context.frame != previous_rendered_frame { 583 - let rendered = canvas.render(&composition, render_background); 614 + let rendered = canvas.render(&composition, render_background)?; 584 615 585 616 previous_rendered_beat = context.beat; 586 617 previous_rendered_frame = context.frame; 587 - progress_bar.inc(1); 588 618 589 619 frames_to_write.push((rendered, context.frame, context.ms)) 590 620 } 591 621 } 592 622 593 - frames_to_write 623 + Ok(frames_to_write) 594 624 } 595 625 596 - fn setup_progress_bar(&self) -> ProgressBar { 597 - indicatif::ProgressBar::new(self.total_frames() as u64).with_style( 598 - indicatif::ProgressStyle::with_template( 599 - &(PROGRESS_BARS_STYLE.to_owned() + " ({pos:.bold} frames out of {len})"), 600 - ) 601 - .unwrap() 602 - .progress_chars("== "), 603 - ) 626 + pub fn setup_progress_bar(&self) -> ProgressBar { 627 + ui::setup_progress_bar(self.total_frames() as u64, "Rendering") 604 628 } 605 629 606 630 pub fn render_composition( ··· 610 634 render_background: bool, 611 635 workers_count: usize, 612 636 _preview_only: bool, 613 - ) -> () { 637 + ) -> Result<()> { 614 638 let mut frame_writer_threads = vec![]; 615 639 let mut frames_to_write: Vec<(String, usize, usize)> = vec![]; 616 640 ··· 623 647 self.initial_canvas.grid_size.0 as f32 / self.initial_canvas.grid_size.1 as f32; 624 648 let resolution = self.resolution; 625 649 626 - let progress_bar = self.setup_progress_bar(); 627 - progress_bar.set_message("Rendering frames to SVG"); 650 + self.progress_bar.set_position(0); 651 + self.progress_bar.set_prefix("Rendering"); 652 + self.progress_bar.set_message(""); 628 653 629 - for (frame, no, ms) in self.render_frames(&progress_bar, composition, render_background) { 654 + for (frame, no, ms) in 655 + self.render_frames(&self.progress_bar, composition, render_background)? 656 + { 630 657 frames_to_write.push((frame, no, ms)); 631 658 } 632 659 633 - progress_bar.println(format!("Rendered {} frames to SVG", frames_to_write.len())); 634 - progress_bar.set_message("Rendering SVG frames to PNG"); 635 - progress_bar.set_position(0); 660 + self.progress_bar.log( 661 + "Finished", 662 + &format!("rendering {} frames to SVG", frames_to_write.len()), 663 + ); 636 664 637 665 frames_to_write.retain(|(_, _, ms)| *ms >= self.start_rendering_at); 638 - progress_bar.set_length(frames_to_write.len() as u64); 666 + 667 + self.progress_bar.set_prefix("Converting"); 668 + self.progress_bar 669 + .set_message("converting SVG frames to PNG"); 670 + self.progress_bar.set_position(0); 671 + self.progress_bar.set_length(frames_to_write.len() as u64); 639 672 640 673 for (frame, no, _) in &frames_to_write { 641 674 std::fs::write( ··· 649 682 let frames_output_directory = self.frames_output_directory; 650 683 for i in 0..workers_count { 651 684 let frames_to_write = Arc::clone(&frames_to_write); 652 - let progress_bar = progress_bar.clone(); 685 + let progress_bar = self.progress_bar.clone(); 653 686 frame_writer_threads.push( 654 687 thread::Builder::new() 655 688 .name(format!("worker-{}", i)) ··· 676 709 handle.join().unwrap(); 677 710 } 678 711 679 - progress_bar.finish_and_clear(); 680 - println!("Rendered SVG frames to PNG"); 712 + self.progress_bar.log("Rendered", "SVG frames to PNG"); 713 + self.progress_bar.finish_and_clear(); 681 714 682 - let spinner = SpinState::start("Building video…"); 683 - if let Err(e) = self.build_video(&output_file) { 684 - panic!("Failed to build video: {}", e); 685 - } 715 + let spinner = ui::Spinner::start("Building video…"); 716 + let result = self.build_video(&output_file); 686 717 spinner.end(&format!("Built video to {}", output_file)); 718 + 719 + result 687 720 } 688 721 } 689 722
+18 -18
src/web.rs
··· 1 + #![allow(unused)] 2 + 1 3 use std::sync::Mutex; 2 4 3 5 use once_cell::sync::Lazy; 4 - use wasm_bindgen::{prelude::wasm_bindgen}; 6 + use wasm_bindgen::prelude::wasm_bindgen; 5 7 use wasm_bindgen::{JsValue, UnwrapThrowExt}; 6 8 7 - use crate::{ 8 - examples, Canvas, Color, ColorMapping, Fill, Filter, Layer, 9 - Object, Point, 10 - }; 9 + use crate::{examples, Canvas, Color, ColorMapping, Fill, Filter, Layer, Object, Point}; 11 10 12 11 static WEB_CANVAS: Lazy<Mutex<Canvas>> = Lazy::new(|| Mutex::new(Canvas::default_settings())); 13 12 ··· 37 36 ($($t:tt)*) => (crate::log(&format_args!($($t)*).to_string())) 38 37 } 39 38 40 - 41 39 #[wasm_bindgen] 42 40 pub fn render_image(opacity: f32, color: Color) -> Result<(), JsValue> { 43 41 let mut canvas = examples::dna_analysis_machine(); ··· 67 65 68 66 #[wasm_bindgen] 69 67 pub fn render_canvas_into(selector: String) -> () { 70 - let svgstring = canvas().render(&vec!["*"], false); 68 + let svgstring = canvas().render(&vec!["*"], false).unwrap_throw(); 71 69 append_new_div_inside(svgstring, selector) 72 70 } 73 71 74 72 #[wasm_bindgen] 75 73 pub fn render_canvas_at(selector: String) -> () { 76 - let svgstring = canvas().render(&vec!["*"], false); 74 + let svgstring = canvas().render(&vec!["*"], false).unwrap_throw(); 77 75 replace_content_with(svgstring, selector) 78 76 } 79 77 ··· 129 127 MidiMessage::PedalOn 130 128 } 131 129 } 132 - (MidiEvent::ControlChange, MidiEventData([controller, value, _])) => { 130 + (MidiEvent::ControlChange, MidiEventData([_, controller, value])) => { 133 131 MidiMessage::ControlChange(controller, value.into()) 134 132 } 135 133 } ··· 137 135 } 138 136 139 137 #[wasm_bindgen] 140 - pub fn render_canvas(layers_pattern: Option<String>, render_background: Option<bool>) -> String { 141 - canvas().render( 142 - &match layers_pattern { 143 - Some(ref pattern) => vec![pattern], 144 - None => vec!["*"], 145 - }, 146 - render_background.unwrap_or(false), 147 - ) 138 + pub fn render_canvas(layers_pattern: Option<String>, render_background: Option<bool>) -> () { 139 + canvas() 140 + .render( 141 + &match layers_pattern { 142 + Some(ref pattern) => vec![pattern], 143 + None => vec!["*"], 144 + }, 145 + render_background.unwrap_or(false), 146 + ) 147 + .unwrap_throw(); 148 148 } 149 149 150 150 #[wasm_bindgen] ··· 214 214 #[wasm_bindgen] 215 215 impl LayerWeb { 216 216 pub fn render(&self) -> String { 217 - canvas().render(&vec![&self.name], false) 217 + canvas().render(&vec![&self.name], false).unwrap_throw() 218 218 } 219 219 220 220 pub fn render_into(&self, selector: String) -> () {