From ad4a18d5e372136e9f88d67e87e0a1790669f351 Mon Sep 17 00:00:00 2001 From: Ewen Le Bihan Date: Sun, 5 May 2024 16:47:02 +0200 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Improve=20console=20UI,=20?= =?UTF-8?q?use=20results=20(wip),=20move=20schedule=20hell=20out=20of=20ma?= =?UTF-8?q?in.rs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 62 ++++++++++++ Cargo.toml | 2 + src/animation.rs | 4 +- src/audio.rs | 2 +- src/canvas.rs | 15 ++- src/examples.rs | 6 +- src/fill.rs | 19 ++-- src/lib.rs | 71 ++------------ src/main.rs | 249 ++--------------------------------------------- src/midi.rs | 36 ++++--- src/objects.rs | 4 + src/preview.rs | 11 ++- src/region.rs | 81 ++++++++++----- src/sync.rs | 2 +- src/ui.rs | 71 ++++++++++++++ src/video.rs | 143 ++++++++++++++++----------- src/web.rs | 36 +++---- 17 files changed, 381 insertions(+), 433 deletions(-) create mode 100644 src/ui.rs diff --git a/Cargo.lock b/Cargo.lock index a7f8636..a1285b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,21 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "aho-corasick" version = "1.1.3" @@ -44,6 +59,21 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +[[package]] +name = "backtrace" +version = "0.3.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -220,6 +250,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + [[package]] name = "half" version = "1.8.3" @@ -354,6 +390,15 @@ dependencies = [ "rayon", ] +[[package]] +name = "miniz_oxide" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +dependencies = [ + "adler", +] + [[package]] name = "nanoid" version = "0.4.0" @@ -378,6 +423,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.19.0" @@ -544,6 +598,12 @@ version = "0.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11707871ffa56ce568d4f15dd34c2f891a2aa5e4b3435b99b8f99938492525c3" +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + [[package]] name = "ryu" version = "1.0.17" @@ -607,8 +667,10 @@ name = "shapemaker" version = "1.1.0" dependencies = [ "anyhow", + "backtrace", "chrono", "chrono-human-duration", + "console", "docopt", "getrandom", "handlebars", diff --git a/Cargo.toml b/Cargo.toml index 74b2073..ac02dd3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,8 @@ web-sys = { version = "0.3.4", features = [ ] } once_cell = "1.19.0" nanoid = "0.4.0" +console = { version = "0.15.8", features = ["windows-console-colors"] } +backtrace = "0.3.71" [dev-dependencies] diff --git a/src/animation.rs b/src/animation.rs index 385aa0c..e1c9ecc 100644 --- a/src/animation.rs +++ b/src/animation.rs @@ -1,9 +1,9 @@ use std::fmt::Display; -use crate::{Canvas, Context, LaterHookCondition, RenderFunction}; +use crate::Canvas; /// Arguments: animation progress (from 0.0 to 1.0), canvas, current ms -pub type AnimationUpdateFunction = dyn Fn(f32, &mut Canvas, usize); +pub type AnimationUpdateFunction = dyn Fn(f32, &mut Canvas, usize) -> anyhow::Result<()>; pub struct Animation { pub name: String, diff --git a/src/audio.rs b/src/audio.rs index 974c14b..7e77513 100644 --- a/src/audio.rs +++ b/src/audio.rs @@ -78,7 +78,7 @@ impl Note { impl Display for SyncData { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "SyncData @ {} bpm\n{} stems", self.bpm, self.stems.len()) + write!(f, "SyncData @ {} bpm, {} stems", self.bpm, self.stems.len()) } } diff --git a/src/canvas.rs b/src/canvas.rs index 1d375d2..8d63582 100644 --- a/src/canvas.rs +++ b/src/canvas.rs @@ -1,6 +1,7 @@ use core::panic; use std::{cmp, collections::HashMap, io::Write as _, ops::Range}; +use anyhow::Result; use itertools::Itertools as _; use rand::Rng; @@ -188,7 +189,7 @@ impl Canvas { object_sizes: ObjectSizes::default(), colormap: ColorMapping::default(), layers: vec![], - world_region: Region::new(0, 0, 3, 3), + world_region: Region::new(0, 0, 3, 3).unwrap(), background: None, } } @@ -379,6 +380,7 @@ impl Canvas { } pub fn random_point(&self, region: &Region) -> Point { + region.ensure_nonempty().unwrap(); Point( rand::thread_rng().gen_range(region.x_range()), rand::thread_rng().gen_range(region.y_range()), @@ -458,7 +460,7 @@ impl Canvas { } pub fn aspect_ratio(&self) -> f32 { - return self.height() as f32 / self.width() as f32; + return self.width() as f32 / self.height() as f32; } pub fn remove_all_objects_in(&mut self, region: &Region) { @@ -517,7 +519,7 @@ impl Canvas { ) } - pub fn render(&mut self, layers: &Vec<&str>, render_background: bool) -> String { + pub fn render(&mut self, layers: &Vec<&str>, render_background: bool) -> Result { let background_color = self.background.unwrap_or_default(); let mut svg = svg::Document::new(); if render_background { @@ -550,7 +552,8 @@ impl Canvas { } } - svg.add(defs) + let rendered = svg + .add(defs) .set( "viewBox", format!( @@ -562,6 +565,8 @@ impl Canvas { ) .set("width", self.width()) .set("height", self.height()) - .to_string() + .to_string(); + + Ok(rendered) } } diff --git a/src/examples.rs b/src/examples.rs index 0b3b3b5..6cd9fa7 100644 --- a/src/examples.rs +++ b/src/examples.rs @@ -27,8 +27,8 @@ pub fn dna_analysis_machine() -> Canvas { let draw_in = canvas.world_region.resized(-1, -1); - let splines_area = Region::from_bottomleft(draw_in.bottomleft().translated(2, -1), (3, 3)); - let red_circle_in = Region::from_topright(draw_in.topright().translated(-3, 0), (4, 3)); + let splines_area = Region::from_bottomleft(draw_in.bottomleft().translated(2, -1), (3, 3)).unwrap(); + let red_circle_in = Region::from_topright(draw_in.topright().translated(-3, 0), (4, 3)).unwrap(); let red_circle_at = red_circle_in.random_point_within(); @@ -113,7 +113,7 @@ pub fn dna_analysis_machine() -> Canvas { pub fn title() -> Canvas { let mut canvas = dna_analysis_machine(); - let text_zone = Region::from_topleft(Point(8, 2), (3, 3)); + let text_zone = Region::from_topleft(Point(8, 2), (3, 3)).unwrap(); canvas.remove_all_objects_in(&text_zone); let last_letter_at = &text_zone.bottomright().translated(1, 0); canvas.remove_all_objects_in(&last_letter_at.region()); diff --git a/src/fill.rs b/src/fill.rs index c624705..2fffd51 100644 --- a/src/fill.rs +++ b/src/fill.rs @@ -1,4 +1,3 @@ - use crate::{Color, ColorMapping, RenderCSS}; #[derive(Debug, Clone, Copy)] @@ -17,10 +16,18 @@ pub enum HatchDirection { TopDownDiagonal, } -const PATTERN_SIZE: usize = 8; - impl HatchDirection {} +impl Fill { + pub fn opacify(&self, opacity: f32) -> Self { + match self { + Fill::Solid(color) => Fill::Translucent(*color, opacity), + Fill::Translucent(color, _) => Fill::Translucent(*color, opacity), + _ => self.clone(), + } + } +} + impl RenderCSS for Fill { fn render_fill_css(&self, colormap: &ColorMapping) -> String { match self { @@ -74,10 +81,11 @@ impl Fill { pub fn pattern_id(&self) -> String { if let Fill::Hatched(color, _, thickness, spacing) = self { return format!( - "pattern-{}-{}-{}", + "pattern-{}-{}-{}-{}", self.pattern_name(), color.name(), - thickness + thickness, + spacing ); } String::from("") @@ -146,4 +154,3 @@ impl Fill { } } } - diff --git a/src/lib.rs b/src/lib.rs index 5559841..17d338e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,5 @@ +#![allow(uncommon_codepoints)] + pub mod animation; pub mod audio; pub mod canvas; @@ -13,9 +15,11 @@ pub mod point; pub mod preview; pub mod region; pub mod sync; +pub mod ui; pub mod video; pub mod web; pub use animation::*; +use anyhow::Result; pub use audio::*; pub use canvas::*; pub use color::*; @@ -30,19 +34,11 @@ pub use sync::Syncable; pub use video::*; pub use web::log; -use indicatif::{ProgressBar, ProgressStyle}; use nanoid::nanoid; use std::fs::{self}; -use std::ops::{Add, Div, Range, Sub}; use std::path::PathBuf; -use std::sync::{Arc, Mutex}; -use std::thread::{self, JoinHandle}; -use std::time; use sync::SyncData; -const PROGRESS_BARS_STYLE: &str = - "{spinner:.cyan} {percent:03.bold.cyan}% {msg:<30} [{bar:100.bold.blue/dim.blue}] {eta:.cyan}"; - pub struct Context<'a, AdditionalContext = ()> { pub frame: usize, pub beat: usize, @@ -57,18 +53,6 @@ pub struct Context<'a, AdditionalContext = ()> { pub duration_override: Option, } -pub trait GetOrDefault { - type Item; - fn get_or(&self, index: usize, default: Self::Item) -> Self::Item; -} - -impl GetOrDefault for Vec { - type Item = T; - fn get_or(&self, index: usize, default: T) -> T { - *self.get(index).unwrap_or(&default) - } -} - impl<'a, C> Context<'a, C> { pub fn stem(&self, name: &str) -> StemAtInstant { let stems = &self.syncdata.stems; @@ -76,7 +60,7 @@ impl<'a, C> Context<'a, C> { panic!("No stem named {:?} found.", name); } StemAtInstant { - amplitude: stems[name].amplitude_db.get_or(self.ms, 0.0), + amplitude: *stems[name].amplitude_db.get(self.ms).unwrap_or(&0.0), amplitude_max: stems[name].amplitude_max, velocity_max: stems[name] .notes @@ -90,11 +74,12 @@ impl<'a, C> Context<'a, C> { } } - pub fn dump_stems(&self, to: PathBuf) { - std::fs::create_dir_all(&to); + pub fn dump_stems(&self, to: PathBuf) -> Result<()> { + std::fs::create_dir_all(&to)?; for (name, stem) in self.syncdata.stems.iter() { - fs::write(to.join(name), format!("{:?}", stem)); + fs::write(to.join(name), format!("{:?}", stem))?; } + Ok(()) } pub fn marker(&self) -> String { @@ -185,41 +170,5 @@ impl<'a, C> Context<'a, C> { } } -struct SpinState { - pub spinner: ProgressBar, - pub finished: Arc>, - pub thread: JoinHandle<()>, -} - -impl SpinState { - fn start(message: &str) -> Self { - let spinner = ProgressBar::new(0).with_style( - ProgressStyle::with_template(&("{spinner:.cyan} ".to_owned() + message)).unwrap(), - ); - spinner.tick(); - - let thread_spinner = spinner.clone(); - let finished = Arc::new(Mutex::new(false)); - let thread_finished = Arc::clone(&finished); - let spinner_thread = thread::spawn(move || { - while !*thread_finished.lock().unwrap() { - thread_spinner.tick(); - thread::sleep(time::Duration::from_millis(100)); - } - thread_spinner.finish_and_clear(); - }); - - Self { - spinner: spinner.clone(), - finished, - thread: spinner_thread, - } - } - fn end(self, message: &str) { - *self.finished.lock().unwrap() = true; - self.thread.join().unwrap(); - println!("{}", message); - } -} - +#[allow(unused)] fn main() {} diff --git a/src/main.rs b/src/main.rs index b66e6e6..d531c73 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,19 +1,20 @@ +use anyhow::Result; use itertools::Itertools; use shapemaker::{ cli::{canvas_from_cli, cli_args}, *, }; -pub fn main() { - run(cli_args()); +pub fn main() -> Result<()> { + run(cli_args()) } -pub fn run(args: cli::Args) { +pub fn run(args: cli::Args) -> Result<()> { let mut canvas = canvas_from_cli(&args); if args.cmd_image && !args.cmd_video { canvas = examples::dna_analysis_machine(); - let rendered = canvas.render(&vec!["*"], true); + let rendered = canvas.render(&vec!["*"], true)?; if args.arg_file.ends_with(".svg") { std::fs::write(args.arg_file, rendered).unwrap(); } else { @@ -27,249 +28,17 @@ pub fn run(args: cli::Args) { Err(e) => println!("Error saving image: {}", e), } } - return; + return Ok(()); } - let mut video = Video::::new(canvas); + let mut video = Video::<()>::new(canvas); video.duration_override = args.flag_duration.map(|seconds| seconds * 1000); video.start_rendering_at = args.flag_start.unwrap_or_default() * 1000; video.fps = args.flag_fps.unwrap_or(30); - video.audiofile = args.flag_audio.unwrap().into(); - video = video - .init(&|canvas: _, context: _| { - context.extra = State { - bass_pattern_at: Region::from_topleft(Point(6, 3), (3, 3)), - first_kick_happened: false, - }; - canvas.set_background(Color::Black); - - let mut kicks = Layer::new("anchor kick"); - - let fill = Fill::Translucent(Color::White, 0.0); - let circle_at = |x: usize, y: usize| Object::SmallCircle(Point(x, y)); - - let (end_x, end_y) = { - let Point(x, y) = canvas.world_region.end; - (x - 2, y - 2) - }; - kicks.set_object("top left", circle_at(1, 1).color(fill)); - kicks.set_object("top right", circle_at(end_x, 1).color(fill)); - kicks.set_object("bottom left", circle_at(1, end_y).color(fill)); - kicks.set_object("bottom right", circle_at(end_x, end_y).color(fill)); - canvas.add_or_replace_layer(kicks); - - let mut ch = Layer::new("ch"); - ch.set_object("0", Object::Dot(Point(0, 0)).into()); - canvas.add_or_replace_layer(ch); - }) - .sync_audio_with(&args.flag_sync_with.unwrap()) - .on_note("anchor kick", &|canvas, ctx| { - // ctx.extra.bass_pattern_at = region_cycle(&canvas.world_region, None); - canvas - .layer("anchor kick") - .paint_all_objects(Fill::Translucent(Color::White, 1.0)); - - canvas.layer("anchor kick").flush(); - - // ctx.later_ms(200, &fade_out_kick_circles) - ctx.animate(200, &|t, canvas, _| { - canvas - .layer("anchor kick") - .paint_all_objects(Fill::Translucent(Color::White, 1.0 - t)); - canvas.layer("anchor kick").flush(); - }); - }) - .on_note("bass", &|canvas, ctx| { - let mut new_layer = canvas.random_layer_within("bass", &ctx.extra.bass_pattern_at); - new_layer.paint_all_objects(Fill::Solid(Color::White)); - canvas.add_or_replace_layer(new_layer); - }) - .on_note("powerful clap hit, clap, perclap", &|canvas, ctx| { - let mut new_layer = - canvas.random_layer_within("claps", &ctx.extra.bass_pattern_at.translated(2, 0)); - new_layer.paint_all_objects(Fill::Solid(Color::Red)); - canvas.add_or_replace_layer(new_layer) - }) - .on_note( - "rimshot, glitchy percs, hitting percs, glitchy percs", - &|canvas, ctx| { - let mut new_layer = canvas - .random_layer_within("percs", &ctx.extra.bass_pattern_at.translated(2, 0)); - new_layer.paint_all_objects(Fill::Translucent(Color::Red, 0.5)); - canvas.add_or_replace_layer(new_layer); - }, - ) - .on_note("qanda", &|canvas, ctx| { - let mut new_layer = canvas.random_linelikes_within( - "qanda", - &ctx.extra.bass_pattern_at.translated(-1, -1).enlarged(1, 1), - ); - new_layer.paint_all_objects(Fill::Solid(Color::Orange)); - new_layer.object_sizes.default_line_width = canvas.object_sizes.default_line_width - * 4.0 - * ctx.stem("qanda").velocity_relative(); - canvas.add_or_replace_layer(new_layer) - }) - .on_note("brokenup", &|canvas, ctx| { - let mut new_layer = canvas - .random_linelikes_within("brokenup", &ctx.extra.bass_pattern_at.translated(0, -2)); - new_layer.paint_all_objects(Fill::Solid(Color::Yellow)); - new_layer.object_sizes.default_line_width = canvas.object_sizes.default_line_width - * 4.0 - * ctx.stem("brokenup").velocity_relative(); - canvas.add_or_replace_layer(new_layer); - }) - .on_note("goup", &|canvas, ctx| { - let mut new_layer = - canvas.random_linelikes_within("goup", &ctx.extra.bass_pattern_at.translated(0, 2)); - new_layer.paint_all_objects(Fill::Solid(Color::Green)); - new_layer.object_sizes.default_line_width = - canvas.object_sizes.default_line_width * 4.0 * ctx.stem("goup").velocity_relative(); - canvas.add_or_replace_layer(new_layer); - }) - .on_note("ch", &|canvas, ctx| { - let world = canvas.world_region.clone(); - - // keep only the last 2 dots - let dots_to_keep = canvas - .layer("ch") - .objects - .iter() - .sorted_by_key(|(name, _)| name.parse::().unwrap()) - .rev() - .take(2) - .map(|(name, _)| (name.clone())) - .collect::>(); - - let layer = canvas.layer("ch"); - layer.object_sizes.empty_shape_stroke_width = 2.0; - layer.objects.retain(|name, _| dots_to_keep.contains(name)); - - let object_name = format!("{}", ctx.ms); - layer.set_object( - &object_name, - Object::Dot(world.resized(-1, -1).random_coordinates_within().into()) - .color(Fill::Solid(Color::Cyan)), - ); - - canvas.put_layer_on_top("ch"); - canvas.layer("ch").flush(); - }) - .when_remaining(10, &|canvas, _| { - canvas.root().set_object( - "credits text", - Object::RawSVG(Box::new(svg::node::Text::new("by ewen-lbh"))).into(), - ); - }) - .command("remove", &|argumentsline, canvas, _| { - let args = argumentsline.splitn(3, ' ').collect::>(); - canvas.remove_object(args[0]); - }); if args.flag_preview { - video.preview_on(8888); - } else { - video.render_to(args.arg_file, args.flag_workers.unwrap_or(8), false); - } -} - -fn fade_out_kick_circles(canvas: &mut Canvas, _: usize) { - canvas - .layer("anchor kick") - .paint_all_objects(Fill::Translucent(Color::White, 0.0)); - - canvas.layer("anchor kick").flush(); -} - -fn update_stem_position( - ctx: &mut shapemaker::Context, - canvas: &mut Canvas, - layer_name: &str, - offset: usize, -) { - let (dx, dy) = ctx.extra.bass_pattern_at - - region_cycle_with_offset( - &canvas.world_region, - Some(&ctx.extra.bass_pattern_at), - offset, - ); - match canvas.layer_safe(layer_name) { - Some(l) => l.move_all_objects(dx, dy), - _ => (), - } -} - -#[derive(Default)] -struct State { - first_kick_happened: bool, - bass_pattern_at: Region, -} - -fn color_cycle(current_color: Color) -> Color { - match current_color { - Color::Blue => Color::Cyan, - Color::Cyan => Color::Green, - Color::Green => Color::Yellow, - Color::Yellow => Color::Orange, - Color::Orange => Color::Red, - Color::Red => Color::Purple, - Color::Purple => Color::Pink, - Color::Pink => Color::White, - Color::White => Color::Blue, - _ => unreachable!(), - } -} - -fn region_cycle_with_offset(world: &Region, current: Option<&Region>, offset: usize) -> Region { - if offset == 0 { - return current.unwrap().clone(); - } - - if offset == 1 { - return region_cycle(world, current); - } - - region_cycle_with_offset(world, current, offset - 1) -} - -fn hat_region_cycle(world: &Region, current: &Region) -> (i32, i32) { - let (end_x, end_y) = { - let Point(x, y) = world.end; - (x - 2, y - 2) - }; - - match current.start { - // top row - Point(x, 1) if x < end_x => (1, 0), - // right column - Point(x, y) if x == end_x && y < end_y => (0, 1), - // bottom row - Point(x, y) if y == end_y && x > 1 => (-1, 0), - // left column - Point(1, y) if y > 1 => (0, -1), - _ => unreachable!(), - } -} - -fn region_cycle(world: &Region, current: Option<&Region>) -> Region { - let mut region = if let Some(current) = current { - current.clone() + video.preview_on(8888) } else { - Region::from_topleft(Point(1, 1), (2, 2)) - }; - - let size = (region.width(), region.height()); - // Move along x axis if possible - if region.end.0 + size.0 <= world.end.0 - 1 { - region.translate(size.0 as i32, 0) - } - // Else go to x=0 and move along y axis - else if region.end.1 + size.1 <= world.end.1 - 1 { - region = Region::new(2, region.end.1, size.0 + 2, region.end.1 + size.1) - } - // Else go to origin - else { - region = Region::from_topleft(Point(1, 1), size) + video.render_to(args.arg_file, args.flag_workers.unwrap_or(8), false) } - region } diff --git a/src/midi.rs b/src/midi.rs index fb5d53a..3edcaf4 100644 --- a/src/midi.rs +++ b/src/midi.rs @@ -1,3 +1,4 @@ +use indicatif::ProgressBar; use itertools::Itertools; use midly::{MetaMessage, MidiMessage, TrackEvent, TrackEventKind}; use std::{collections::HashMap, fmt::Debug, path::PathBuf}; @@ -18,10 +19,6 @@ impl Averageable for Vec { } } -fn is_kick_channel(name: &str) -> bool { - name.contains("kick") -} - impl Syncable for MidiSynchronizer { fn new(path: &str) -> Self { Self { @@ -29,8 +26,8 @@ impl Syncable for MidiSynchronizer { } } - fn load(&self) -> SyncData { - let (now, notes_per_instrument) = load_notes(&self.midi_path); + fn load(&self, progressbar: Option<&ProgressBar>) -> SyncData { + let (now, notes_per_instrument) = load_notes(&self.midi_path, progressbar); SyncData { bpm: tempo_to_bpm(now.tempo), @@ -139,12 +136,20 @@ impl Debug for Note { } } -fn load_notes<'a>(source: &PathBuf) -> (Now, HashMap>) { +fn load_notes<'a>( + source: &PathBuf, + progressbar: Option<&ProgressBar>, +) -> (Now, HashMap>) { // Read midi file using midly + if let Some(pb) = progressbar { + pb.set_length(1); + pb.set_prefix("Loading"); + pb.set_message("reading MIDI file"); + pb.set_position(0); + } + let raw = std::fs::read(source).unwrap(); let midifile = midly::Smf::parse(&raw).unwrap(); - println!("# of tracks\n\t{}", midifile.tracks.len()); - println!("{:#?}", midifile.header); let mut timeline = Timeline::new(); let mut now = Now { @@ -156,6 +161,7 @@ fn load_notes<'a>(source: &PathBuf) -> (Now, HashMap>) { }, }; + // Get track names let mut track_no = 0; let mut track_names = HashMap::::new(); @@ -180,8 +186,6 @@ fn load_notes<'a>(source: &PathBuf) -> (Now, HashMap>) { ); } - println!("{:#?}", track_names); - // Convert ticks to absolute let mut track_no = 0; for track in midifile.tracks.iter() { @@ -215,6 +219,13 @@ fn load_notes<'a>(source: &PathBuf) -> (Now, HashMap>) { absolute_tick_to_ms.insert(*tick, now.ms); } + if let Some(ref pb) = progressbar { + pb.set_length(midifile.tracks.iter().map(|t| t.len()).sum::() as u64); + pb.set_prefix("Loading"); + pb.set_message("parsing MIDI events"); + pb.set_position(0); + } + // Add notes let mut stem_notes = StemNotes::new(); for (tick, tracks) in timeline.iter().sorted_by_key(|(tick, _)| *tick) { @@ -246,6 +257,9 @@ fn load_notes<'a>(source: &PathBuf) -> (Now, HashMap>) { }, _ => {} } + if let Some(ref pb) = progressbar { + pb.inc(1); + } } } diff --git a/src/objects.rs b/src/objects.rs index 1a1a5fb..894bfd7 100644 --- a/src/objects.rs +++ b/src/objects.rs @@ -47,6 +47,10 @@ impl ColoredObject { pub fn clear_filters(&mut self) { self.2.clear(); } + + pub fn fill(&self) -> Option { + self.1 + } } impl std::fmt::Display for ColoredObject { diff --git a/src/preview.rs b/src/preview.rs index 3da4ef3..3be82c9 100644 --- a/src/preview.rs +++ b/src/preview.rs @@ -1,5 +1,6 @@ use std::{collections::HashMap, fs, path::PathBuf}; +use anyhow::Result; use handlebars::Handlebars; use itertools::Itertools; use serde_json::json; @@ -39,7 +40,7 @@ pub fn output_preview( server_port: usize, output_file: PathBuf, audio_file: PathBuf, -) { +) -> Result<()> { let first_frames = rendered_svg_frames .iter() // over 3000 loaded frames get really heavy on the browser (too much DOM nodes) @@ -49,10 +50,11 @@ pub fn output_preview( .collect::>(); let contents = render_template(&first_frames, canvas, audio_file, server_port); - fs::write(output_file, contents); + fs::write(output_file, contents)?; + Ok(()) } -pub fn start_preview_server(port: usize, frames: HashMap) { +pub fn start_preview_server(port: usize, frames: HashMap) -> Result<()> { let server = tiny_http::Server::http(format!("0.0.0.0:{}", port)).unwrap(); println!("Preview server running on port {}", port); let sorted_frames: Vec<(&usize, &String)> = @@ -84,8 +86,9 @@ pub fn start_preview_server(port: usize, frames: HashMap) { field: "Access-Control-Allow-Origin".parse().unwrap(), value: "*".parse().unwrap(), }, - )); + ))?; } + Ok(()) } // returns (ms timestamp of first frame to send, number of frames to send) diff --git a/src/region.rs b/src/region.rs index b6d4c97..705c647 100644 --- a/src/region.rs +++ b/src/region.rs @@ -1,4 +1,6 @@ use crate::Point; +use anyhow::{format_err, Error, Result}; +use backtrace::Backtrace; use rand::Rng; use wasm_bindgen::prelude::*; @@ -36,6 +38,14 @@ impl Region { } } } + + pub fn ensure_nonempty(&self) -> Result<()> { + if self.width() == 0 || self.height() == 0 { + return Err(format_err!("Region {} is empty", self)); + } + + Ok(()) + } } pub struct RegionIterator { @@ -109,7 +119,7 @@ impl std::ops::Sub for Region { #[test] fn test_sub_and_transate_coherence() { - let a = Region::from_origin(Point(3, 3)); + let a = Region::from_origin(Point(3, 3)).unwrap(); let mut b = a.clone(); b.translate(2, 3); @@ -117,13 +127,16 @@ fn test_sub_and_transate_coherence() { } impl Region { - pub fn new(start_x: usize, start_y: usize, end_x: usize, end_y: usize) -> Self { + pub fn new(start_x: usize, start_y: usize, end_x: usize, end_y: usize) -> Result { let region = Self { start: (start_x, start_y).into(), end: (end_x, end_y).into(), }; - region.ensure_valid(); - region + region.ensure_valid() + } + + pub fn from_points(start: Point, end: Point) -> Result { + Self::new(start.0, start.1, end.0, end.1) } pub fn bottomleft(&self) -> Point { @@ -157,33 +170,33 @@ impl Region { ) } - pub fn from_origin(end: Point) -> Self { + pub fn from_origin(end: Point) -> Result { Self::new(0, 0, end.0, end.1) } - pub fn from_topleft(origin: Point, size: (usize, usize)) -> Self { - Self::from(( + pub fn from_topleft(origin: Point, size: (usize, usize)) -> Result { + Self::from_points( origin, origin.translated_by(Point::from(size).translated(-1, -1)), - )) + ) } - pub fn from_bottomleft(origin: Point, size: (usize, usize)) -> Self { + pub fn from_bottomleft(origin: Point, size: (usize, usize)) -> Result { Self::from_topleft(origin.translated(0, -(size.1 as i32 - 1)), size) } - pub fn from_bottomright(origin: Point, size: (usize, usize)) -> Self { - Self::from(( + pub fn from_bottomright(origin: Point, size: (usize, usize)) -> Result { + Self::from_points( origin.translated_by(Point::from(size).translated(-1, -1)), origin, - )) + ) } - pub fn from_topright(origin: Point, size: (usize, usize)) -> Self { + pub fn from_topright(origin: Point, size: (usize, usize)) -> Result { Self::from_topleft(origin.translated(-(size.0 as i32 - 1), 0), size) } - pub fn from_center_and_size(center: Point, size: (usize, usize)) -> Self { + pub fn from_center_and_size(center: Point, size: (usize, usize)) -> Result { let half_size = (size.0 / 2, size.1 / 2); Self::new( center.0 - half_size.0, @@ -194,14 +207,16 @@ impl Region { } // panics if the region is invalid - pub fn ensure_valid(self) -> Self { + pub fn ensure_valid(self) -> Result { if self.start.0 >= self.end.0 || self.start.1 >= self.end.1 { - panic!( + return Err(format_err!( "Invalid region: start ({:?}) >= end ({:?})", - self.start, self.end - ) + self.start, + self.end + )); } - self + + Ok(self) } pub fn translate(&mut self, dx: i32, dy: i32) { @@ -211,30 +226,36 @@ impl Region { pub fn translated(&self, dx: i32, dy: i32) -> Self { Self { start: ( - (self.start.0 as i32 + dx) as usize, - (self.start.1 as i32 + dy) as usize, + (self.start.0 as i32 + dx).max(0) as usize, + (self.start.1 as i32 + dy).max(0) as usize, ) .into(), end: ( - (self.end.0 as i32 + dx) as usize, - (self.end.1 as i32 + dy) as usize, + (self.end.0 as i32 + dx).max(0) as usize, + (self.end.1 as i32 + dy).max(0) as usize, ) .into(), } - .ensure_valid() } /// adds dx and dy to the end of the region (dx and dy are _not_ multiplicative but **additive** factors) pub fn enlarged(&self, dx: i32, dy: i32) -> Self { - Self { + let resulting = Self { start: self.start, end: ( (self.end.0 as i32 + dx) as usize, (self.end.1 as i32 + dy) as usize, ) .into(), + }; + + if resulting.ensure_valid().is_err() { + let bt = Backtrace::new(); + println!("WARN: Did not enlarge region {self} with ({dx}, {dy}), it would result in a non-valid region\n{bt:?}"); + return *self; } - .ensure_valid() + + resulting } /// resized is like enlarged, but transforms from the center, by first translating the region by (-dx, -dy) @@ -276,10 +297,18 @@ impl Region { } pub fn width(&self) -> usize { + if self.end.0 < self.start.0 { + return 0; + } + self.end.0 - self.start.0 + 1 } pub fn height(&self) -> usize { + if self.end.1 < self.start.1 { + return 0; + } + self.end.1 - self.start.1 + 1 } diff --git a/src/sync.rs b/src/sync.rs index 68dc0fc..12e7096 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -5,8 +5,8 @@ use crate::Stem; pub type TimestampMS = usize; pub trait Syncable { - fn load(&self) -> SyncData; fn new(path: &str) -> Self; + fn load(&self, progress: Option<&indicatif::ProgressBar>) -> SyncData; } #[derive(Debug, Default)] diff --git a/src/ui.rs b/src/ui.rs new file mode 100644 index 0000000..bc35727 --- /dev/null +++ b/src/ui.rs @@ -0,0 +1,71 @@ +use console::Style; +use indicatif::{ProgressBar, ProgressStyle}; +use std::sync::{Arc, Mutex}; +use std::thread::{self, JoinHandle}; +use std::time; + +pub const PROGRESS_BARS_STYLE: &str = + "{prefix:>12.bold.cyan} [{bar:40}] {pos}/{len}: {msg} ({eta} left)"; + +pub struct Spinner { + pub spinner: ProgressBar, + pub finished: Arc>, + pub thread: JoinHandle<()>, +} + +impl Spinner { + pub fn start(message: &str) -> Self { + let spinner = ProgressBar::new(0).with_style( + ProgressStyle::with_template(&("{spinner:.cyan} ".to_owned() + message)).unwrap(), + ); + spinner.tick(); + + let thread_spinner = spinner.clone(); + let finished = Arc::new(Mutex::new(false)); + let thread_finished = Arc::clone(&finished); + let spinner_thread = thread::spawn(move || { + while !*thread_finished.lock().unwrap() { + thread_spinner.tick(); + thread::sleep(time::Duration::from_millis(100)); + } + thread_spinner.finish_and_clear(); + }); + + Self { + spinner: spinner.clone(), + finished, + thread: spinner_thread, + } + } + + pub fn end(self, message: &str) { + *self.finished.lock().unwrap() = true; + self.thread.join().unwrap(); + println!("{}", message); + } +} + +pub fn setup_progress_bar(total: u64, verb: &'static str) -> ProgressBar { + indicatif::ProgressBar::new(total) + .with_prefix(verb) + .with_style( + indicatif::ProgressStyle::with_template(PROGRESS_BARS_STYLE) + .unwrap() + .progress_chars("=> "), + ) +} + +pub trait Log { + fn log(&self, verb: &'static str, message: &str); +} + +pub fn format_log_msg(verb: &'static str, message: &str) -> String { + let style = Style::new().bold().cyan(); + format!("{}: {}", style.apply_to(format!("{verb:>12}")), message) +} + +impl Log for ProgressBar { + fn log(&self, verb: &'static str, message: &str) { + self.println(format_log_msg(verb, message)); + } +} diff --git a/src/video.rs b/src/video.rs index ecaed30..a43b5d8 100644 --- a/src/video.rs +++ b/src/video.rs @@ -3,30 +3,39 @@ use std::{ collections::HashMap, fmt::Formatter, fs::{create_dir, create_dir_all, remove_dir_all}, + panic, path::{Path, PathBuf}, sync::Arc, - thread, }; +use std::thread; + +use anyhow::Result; use chrono::{DateTime, NaiveDateTime}; -use indicatif::ProgressBar; +use indicatif::{ProgressBar, ProgressIterator}; use crate::{ - preview, sync::SyncData, Canvas, Context, Fill, MidiSynchronizer, MusicalDurationUnit, Object, - SpinState, Syncable, PROGRESS_BARS_STYLE, + preview, + sync::SyncData, + ui::{self, setup_progress_bar, Log as _}, + Canvas, ColoredObject, Context, MidiSynchronizer, MusicalDurationUnit, Syncable, }; -pub type RenderFunction = dyn Fn(&mut Canvas, &mut Context); -pub type CommandAction = dyn Fn(String, &mut Canvas, &mut Context); +pub type BeatNumber = usize; +pub type FrameNumber = usize; +pub type Millisecond = usize; + +pub type RenderFunction = dyn Fn(&mut Canvas, &mut Context) -> anyhow::Result<()>; +pub type CommandAction = dyn Fn(String, &mut Canvas, &mut Context) -> anyhow::Result<()>; /// Arguments: canvas, context, previous rendered beat, previous rendered frame -pub type HookCondition = dyn Fn(&Canvas, &Context, usize, usize) -> bool; +pub type HookCondition = dyn Fn(&Canvas, &Context, BeatNumber, FrameNumber) -> bool; /// Arguments: canvas, context, current milliseconds timestamp -pub type LaterRenderFunction = dyn Fn(&mut Canvas, usize); +pub type LaterRenderFunction = dyn Fn(&mut Canvas, Millisecond) -> anyhow::Result<()>; /// Arguments: canvas, context, previous rendered beat -pub type LaterHookCondition = dyn Fn(&Canvas, &Context, usize) -> bool; +pub type LaterHookCondition = dyn Fn(&Canvas, &Context, BeatNumber) -> bool; #[derive(Debug)] pub struct Video { @@ -41,6 +50,7 @@ pub struct Video { pub resolution: usize, pub duration_override: Option, pub start_rendering_at: usize, + pub progress_bar: indicatif::ProgressBar, } pub struct Hook { pub when: Box>, @@ -97,21 +107,22 @@ impl Video { audiofile: PathBuf::new(), duration_override: None, start_rendering_at: 0, + progress_bar: setup_progress_bar(0, ""), } } pub fn sync_audio_with(self, sync_data_path: &str) -> Self { if sync_data_path.ends_with(".mid") || sync_data_path.ends_with(".midi") { let loader = MidiSynchronizer::new(sync_data_path); - let syncdata = loader.load(); - println!("Loaded MIDI sync data: {}", syncdata); + let syncdata = loader.load(Some(&self.progress_bar)); + self.progress_bar.finish(); return Self { syncdata, ..self }; } panic!("Unsupported sync data format"); } - pub fn build_video(&self, render_to: &str) -> Result<(), String> { + pub fn build_video(&self, render_to: &str) -> Result<()> { let mut command = std::process::Command::new("ffmpeg"); command @@ -140,7 +151,7 @@ impl Video { println!("Running command: {:?}", command); match command.output() { - Err(e) => Err(format!("Failed to execute ffmpeg: {}", e)), + Err(e) => Err(anyhow::format_err!("Failed to execute ffmpeg: {}", e).into()), Ok(r) => { println!("{}", std::str::from_utf8(&r.stdout).unwrap()); println!("{}", std::str::from_utf8(&r.stderr).unwrap()); @@ -316,7 +327,7 @@ impl Video { create_object: &'static dyn Fn( &Canvas, &mut Context, - ) -> (Object, Option), + ) -> Result, ) -> Self { self.with_hook(Hook { when: Box::new(move |_, ctx, _, _| { @@ -325,8 +336,9 @@ impl Video { .any(|stem_name| ctx.stem(stem_name).notes.iter().any(|note| note.is_on())) }), render_function: Box::new(move |canvas, ctx| { - let (object, fill) = create_object(canvas, ctx); - canvas.add_object(layer_name, object_name, object, fill); + let object = create_object(canvas, ctx)?; + canvas.layer(&layer_name).set_object(object_name, object); + Ok(()) }), }) .with_hook(Hook { @@ -336,7 +348,10 @@ impl Video { || ctx.stem(stem_name).notes.iter().any(|note| note.is_off()) }) }), - render_function: Box::new(move |canvas, _| canvas.remove_object(object_name)), + render_function: Box::new(move |canvas, _| { + canvas.remove_object(object_name); + Ok(()) + }), }) } @@ -451,11 +466,11 @@ impl Video { .unwrap() } - pub fn preview_on(&self, port: usize) { + pub fn preview_on(&self, port: usize) -> Result<()> { let mut rendered_frames: HashMap = HashMap::new(); let progress_bar = self.setup_progress_bar(); - for (frame, _, ms) in self.render_frames(&progress_bar, vec!["*"], true) { + for (frame, _, ms) in self.render_frames(&progress_bar, vec!["*"], true)? { rendered_frames.insert(ms, frame); } @@ -467,15 +482,21 @@ impl Video { port, PathBuf::from(".").join("preview.html"), self.audiofile.clone(), - ); - preview::start_preview_server(port, rendered_frames); + )?; + + preview::start_preview_server(port, rendered_frames) } - pub fn render_to(&self, output_file: String, workers_count: usize, preview_only: bool) -> () { - self.render_composition(output_file, vec!["*"], true, workers_count, preview_only); + pub fn render_to( + &self, + output_file: String, + workers_count: usize, + preview_only: bool, + ) -> Result<()> { + self.render_composition(output_file, vec!["*"], true, workers_count, preview_only) } - pub fn render_layers_in(&self, output_directory: String, workers_count: usize) -> () { + pub fn render_layers_in(&self, output_directory: String, workers_count: usize) -> Result<()> { for composition in self .initial_canvas .layers @@ -488,8 +509,9 @@ impl Video { false, workers_count, false, - ); + )?; } + Ok(()) } // Returns a triple of (SVG content, frame number, millisecond at frame) @@ -498,7 +520,7 @@ impl Video { progress_bar: &ProgressBar, composition: Vec<&str>, render_background: bool, - ) -> Vec<(String, usize, usize)> { + ) -> Result> { let mut context = Context { frame: 0, beat: 0, @@ -519,13 +541,22 @@ impl Video { let mut previous_rendered_frame = 0; let mut frames_to_write: Vec<(String, usize, usize)> = vec![]; - for _ in 0..self.duration_ms() + self.start_rendering_at { + let render_ms_range = 0..self.duration_ms() + self.start_rendering_at; + + self.progress_bar.set_length(render_ms_range.len() as u64); + + for _ in render_ms_range + .into_iter() + .progress_with(self.progress_bar.clone()) + { context.ms += 1_usize; context.timestamp = milliseconds_to_timestamp(context.ms).to_string(); context.beat_fractional = (context.bpm * context.ms) as f32 / (1000.0 * 60.0); context.beat = context.beat_fractional as usize; context.frame = ((self.fps * context.ms) as f64 / 1000.0) as usize; + progress_bar.set_message(context.timestamp.clone()); + if context.marker() != "" { progress_bar.println(format!( "{}: marker {}", @@ -556,7 +587,7 @@ impl Video { previous_rendered_beat, previous_rendered_frame, ) { - (hook.render_function)(&mut canvas, &mut context); + (hook.render_function)(&mut canvas, &mut context)?; } } @@ -564,7 +595,7 @@ impl Video { for (i, hook) in context.later_hooks.iter().enumerate() { if (hook.when)(&canvas, &context, previous_rendered_beat) { - (hook.render_function)(&mut canvas, context.ms); + (hook.render_function)(&mut canvas, context.ms)?; if hook.once { later_hooks_to_delete.push(i); } @@ -580,27 +611,20 @@ impl Video { } if context.frame != previous_rendered_frame { - let rendered = canvas.render(&composition, render_background); + let rendered = canvas.render(&composition, render_background)?; previous_rendered_beat = context.beat; previous_rendered_frame = context.frame; - progress_bar.inc(1); frames_to_write.push((rendered, context.frame, context.ms)) } } - frames_to_write + Ok(frames_to_write) } - fn setup_progress_bar(&self) -> ProgressBar { - indicatif::ProgressBar::new(self.total_frames() as u64).with_style( - indicatif::ProgressStyle::with_template( - &(PROGRESS_BARS_STYLE.to_owned() + " ({pos:.bold} frames out of {len})"), - ) - .unwrap() - .progress_chars("== "), - ) + pub fn setup_progress_bar(&self) -> ProgressBar { + ui::setup_progress_bar(self.total_frames() as u64, "Rendering") } pub fn render_composition( @@ -610,7 +634,7 @@ impl Video { render_background: bool, workers_count: usize, _preview_only: bool, - ) -> () { + ) -> Result<()> { let mut frame_writer_threads = vec![]; let mut frames_to_write: Vec<(String, usize, usize)> = vec![]; @@ -623,19 +647,28 @@ impl Video { self.initial_canvas.grid_size.0 as f32 / self.initial_canvas.grid_size.1 as f32; let resolution = self.resolution; - let progress_bar = self.setup_progress_bar(); - progress_bar.set_message("Rendering frames to SVG"); + self.progress_bar.set_position(0); + self.progress_bar.set_prefix("Rendering"); + self.progress_bar.set_message(""); - for (frame, no, ms) in self.render_frames(&progress_bar, composition, render_background) { + for (frame, no, ms) in + self.render_frames(&self.progress_bar, composition, render_background)? + { frames_to_write.push((frame, no, ms)); } - progress_bar.println(format!("Rendered {} frames to SVG", frames_to_write.len())); - progress_bar.set_message("Rendering SVG frames to PNG"); - progress_bar.set_position(0); + self.progress_bar.log( + "Finished", + &format!("rendering {} frames to SVG", frames_to_write.len()), + ); frames_to_write.retain(|(_, _, ms)| *ms >= self.start_rendering_at); - progress_bar.set_length(frames_to_write.len() as u64); + + self.progress_bar.set_prefix("Converting"); + self.progress_bar + .set_message("converting SVG frames to PNG"); + self.progress_bar.set_position(0); + self.progress_bar.set_length(frames_to_write.len() as u64); for (frame, no, _) in &frames_to_write { std::fs::write( @@ -649,7 +682,7 @@ impl Video { let frames_output_directory = self.frames_output_directory; for i in 0..workers_count { let frames_to_write = Arc::clone(&frames_to_write); - let progress_bar = progress_bar.clone(); + let progress_bar = self.progress_bar.clone(); frame_writer_threads.push( thread::Builder::new() .name(format!("worker-{}", i)) @@ -676,14 +709,14 @@ impl Video { handle.join().unwrap(); } - progress_bar.finish_and_clear(); - println!("Rendered SVG frames to PNG"); + self.progress_bar.log("Rendered", "SVG frames to PNG"); + self.progress_bar.finish_and_clear(); - let spinner = SpinState::start("Building video…"); - if let Err(e) = self.build_video(&output_file) { - panic!("Failed to build video: {}", e); - } + let spinner = ui::Spinner::start("Building video…"); + let result = self.build_video(&output_file); spinner.end(&format!("Built video to {}", output_file)); + + result } } diff --git a/src/web.rs b/src/web.rs index 0e6077b..86ed0e2 100644 --- a/src/web.rs +++ b/src/web.rs @@ -1,13 +1,12 @@ +#![allow(unused)] + use std::sync::Mutex; use once_cell::sync::Lazy; -use wasm_bindgen::{prelude::wasm_bindgen}; +use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::{JsValue, UnwrapThrowExt}; -use crate::{ - examples, Canvas, Color, ColorMapping, Fill, Filter, Layer, - Object, Point, -}; +use crate::{examples, Canvas, Color, ColorMapping, Fill, Filter, Layer, Object, Point}; static WEB_CANVAS: Lazy> = Lazy::new(|| Mutex::new(Canvas::default_settings())); @@ -37,7 +36,6 @@ macro_rules! console_log { ($($t:tt)*) => (crate::log(&format_args!($($t)*).to_string())) } - #[wasm_bindgen] pub fn render_image(opacity: f32, color: Color) -> Result<(), JsValue> { let mut canvas = examples::dna_analysis_machine(); @@ -67,13 +65,13 @@ pub fn map_to_midi_controller() {} #[wasm_bindgen] pub fn render_canvas_into(selector: String) -> () { - let svgstring = canvas().render(&vec!["*"], false); + let svgstring = canvas().render(&vec!["*"], false).unwrap_throw(); append_new_div_inside(svgstring, selector) } #[wasm_bindgen] pub fn render_canvas_at(selector: String) -> () { - let svgstring = canvas().render(&vec!["*"], false); + let svgstring = canvas().render(&vec!["*"], false).unwrap_throw(); replace_content_with(svgstring, selector) } @@ -129,7 +127,7 @@ impl From<(MidiEvent, MidiEventData)> for MidiMessage { MidiMessage::PedalOn } } - (MidiEvent::ControlChange, MidiEventData([controller, value, _])) => { + (MidiEvent::ControlChange, MidiEventData([_, controller, value])) => { MidiMessage::ControlChange(controller, value.into()) } } @@ -137,14 +135,16 @@ impl From<(MidiEvent, MidiEventData)> for MidiMessage { } #[wasm_bindgen] -pub fn render_canvas(layers_pattern: Option, render_background: Option) -> String { - canvas().render( - &match layers_pattern { - Some(ref pattern) => vec![pattern], - None => vec!["*"], - }, - render_background.unwrap_or(false), - ) +pub fn render_canvas(layers_pattern: Option, render_background: Option) -> () { + canvas() + .render( + &match layers_pattern { + Some(ref pattern) => vec![pattern], + None => vec!["*"], + }, + render_background.unwrap_or(false), + ) + .unwrap_throw(); } #[wasm_bindgen] @@ -214,7 +214,7 @@ pub struct LayerWeb { #[wasm_bindgen] impl LayerWeb { pub fn render(&self) -> String { - canvas().render(&vec![&self.name], false) + canvas().render(&vec![&self.name], false).unwrap_throw() } pub fn render_into(&self, selector: String) -> () {