This repository has no description
0

Configure Feed

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

♻️ Cleanup renderable traits, make submodules

+1718 -1892
src/animation.rs src/video/animation.rs
+2 -4
src/audio.rs src/synchronization/audio.rs
··· 1 + use super::sync::SyncData; 2 + use serde::{Deserialize, Serialize}; 1 3 use std::{ 2 4 collections::HashMap, 3 5 fmt::Display, ··· 5 7 io::{BufReader, Write}, 6 8 path::{Path, PathBuf}, 7 9 }; 8 - 9 - use serde::{Deserialize, Serialize}; 10 - 11 - use crate::sync::SyncData; 12 10 13 11 #[derive(Debug, Deserialize, Serialize)] 14 12 pub struct Stem {
-282
src/audiosync/stems.rs
··· 1 - // #[deprecated(note = "Use `sync_with` instead")] 2 - // pub fn sync_to(self, audio: &AudioSyncPaths, stem_audio_to_midi: AudioStemToMIDITrack) -> Self { 3 - // let progress_bar_tree = MultiProgress::new(); 4 - // // Read BPM from file 5 - // let bpm = std::fs::read_to_string(audio.bpm.clone()) 6 - // .map_err(|e| format!("Failed to read BPM file: {}", e)) 7 - // .and_then(|bpm| { 8 - // bpm.trim() 9 - // .parse::<usize>() 10 - // .map(|parsed| parsed) 11 - // .map_err(|e| format!("Failed to parse BPM file: {}", e)) 12 - // }) 13 - // .unwrap(); 14 - 15 - // // Read landmakrs from JSON file 16 - // let markers = std::fs::read_to_string(audio.landmarks.clone()) 17 - // .map_err(|e| format!("Failed to read landmarks file: {}", e)) 18 - // .and_then(|landmarks| { 19 - // match serde_json::from_str::<HashMap<String, String>>(&landmarks) 20 - // .map_err(|e| format!("Failed to parse landmarks file: {}", e)) 21 - // { 22 - // Ok(unparsed_keys) => { 23 - // let mut parsed_keys: HashMap<usize, String> = HashMap::new(); 24 - // for (key, value) in unparsed_keys { 25 - // parsed_keys.insert(key.parse::<usize>().unwrap(), value); 26 - // } 27 - // Ok(parsed_keys) 28 - // } 29 - // Err(e) => Err(e), 30 - // } 31 - // }) 32 - // .unwrap(); 33 - 34 - // // Read all WAV stem files: get their duration and amplitude per millisecond 35 - // let mut stems: HashMap<String, Stem> = HashMap::new(); 36 - 37 - // let mut threads = vec![]; 38 - // let (tx, rx) = mpsc::channel(); 39 - 40 - // let stem_file_entries: Vec<_> = std::fs::read_dir(audio.stems.clone()) 41 - // .map_err(|e| format!("Failed to read stems directory: {}", e)) 42 - // .unwrap() 43 - // .filter(|e| match e { 44 - // Ok(e) => e.path().extension().unwrap_or_default() == "wav", 45 - // Err(_) => false, 46 - // }) 47 - // .collect(); 48 - 49 - // let main_progress_bar = progress_bar_tree.add( 50 - // ProgressBar::new(stem_file_entries.len() as u64) 51 - // .with_style( 52 - // ProgressStyle::with_template( 53 - // &(PROGRESS_BARS_STYLE.to_owned() 54 - // + " ({pos:.bold} stems loaded out of {len})"), 55 - // ) 56 - // .unwrap() 57 - // .progress_chars("== "), 58 - // ) 59 - // .with_message("Loading stems"), 60 - // ); 61 - 62 - // main_progress_bar.tick(); 63 - 64 - // for (i, entry) in stem_file_entries.into_iter().enumerate() { 65 - // let progress_bar = progress_bar_tree.add( 66 - // ProgressBar::new(0).with_style( 67 - // ProgressStyle::with_template(&(" ".to_owned() + PROGRESS_BARS_STYLE)) 68 - // .unwrap() 69 - // .progress_chars("== "), 70 - // ), 71 - // ); 72 - // let main_progress_bar = main_progress_bar.clone(); 73 - // let tx = tx.clone(); 74 - // threads.push(thread::spawn(move || { 75 - // let path = entry.unwrap().path(); 76 - // let stem_name: String = path.file_stem().unwrap().to_string_lossy().into(); 77 - // let stem_cache_path = Stem::cbor_path(path.clone(), stem_name.clone()); 78 - // progress_bar.set_message(format!("Loading \"{}\"", stem_name)); 79 - 80 - // // Check if a cached CBOR of the stem file exists 81 - // if Path::new(&stem_cache_path).exists() { 82 - // let stem = Stem::load_from_cbor(&stem_cache_path); 83 - // progress_bar.set_message("Loaded {} from cache".to_owned()); 84 - // tx.send((progress_bar, stem_name, stem)).unwrap(); 85 - // main_progress_bar.inc(1); 86 - // return; 87 - // } 88 - 89 - // let mut reader = hound::WavReader::open(path.clone()) 90 - // .map_err(|e| format!("Failed to read stem file {:?}: {}", path, e)) 91 - // .unwrap(); 92 - // let spec = reader.spec(); 93 - // let sample_to_frame = |sample: usize| { 94 - // (sample as f64 / spec.channels as f64 / spec.sample_rate as f64 95 - // * self.fps as f64) as usize 96 - // }; 97 - // let mut amplitude_db: Vec<f32> = vec![]; 98 - // let mut current_amplitude_sum: f32 = 0.0; 99 - // let mut current_amplitude_buffer_size: usize = 0; 100 - // let mut latest_loaded_frame = 0; 101 - // progress_bar.set_length(reader.samples::<i16>().len() as u64); 102 - // for (i, sample) in reader.samples::<i16>().enumerate() { 103 - // let sample = sample.unwrap(); 104 - // if sample_to_frame(i) > latest_loaded_frame { 105 - // amplitude_db 106 - // .push(current_amplitude_sum / current_amplitude_buffer_size as f32); 107 - // current_amplitude_sum = 0.0; 108 - // current_amplitude_buffer_size = 0; 109 - // latest_loaded_frame = sample_to_frame(i); 110 - // } else { 111 - // current_amplitude_sum += sample.abs() as f32; 112 - // current_amplitude_buffer_size += 1; 113 - // } 114 - // // main_progress_bar.tick(); 115 - // progress_bar.inc(1); 116 - // } 117 - // amplitude_db.push(current_amplitude_sum / current_amplitude_buffer_size as f32); 118 - // progress_bar.finish_with_message(format!(" Loaded \"{}\"", stem_name)); 119 - 120 - // let stem = Stem { 121 - // amplitude_max: *amplitude_db 122 - // .iter() 123 - // .max_by(|a, b| a.partial_cmp(b).unwrap()) 124 - // .unwrap(), 125 - // amplitude_db, 126 - // duration_ms: (reader.duration() as f64 / spec.sample_rate as f64 * 1000.0) 127 - // as usize, 128 - // notes: HashMap::new(), 129 - // path: path.clone(), 130 - // name: stem_name.clone(), 131 - // }; 132 - 133 - // main_progress_bar.inc(1); 134 - 135 - // tx.send((progress_bar, stem_name, stem)).unwrap(); 136 - // drop(tx); 137 - // })); 138 - // } 139 - // drop(tx); 140 - 141 - // for (progress_bar, stem_name, stem) in rx { 142 - // progress_bar.finish_and_clear(); 143 - // stems.insert(stem_name.to_string(), stem); 144 - // } 145 - 146 - // for thread in threads { 147 - // thread.join().unwrap(); 148 - // } 149 - 150 - // // Read MIDI file 151 - // println!("Loading MIDI…"); 152 - // let midi_bytes = std::fs::read(audio.midi.clone()) 153 - // .map_err(|e| format!("While loading MIDI file {}: {:?}", audio.midi.clone(), e)) 154 - // .unwrap(); 155 - // let midi = midly::Smf::parse(&midi_bytes).unwrap(); 156 - 157 - // let mut timeline = HashMap::<u32, HashMap<String, midly::TrackEvent>>::new(); 158 - // let mut now_ms = 0.0; 159 - // let mut now_tempo = 500_000.0; 160 - // let ticks_per_beat = match midi.header.timing { 161 - // midly::Timing::Metrical(ticks_per_beat) => ticks_per_beat.as_int(), 162 - // midly::Timing::Timecode(fps, subframe) => (1.0 / fps.as_f32() / subframe as f32) as u16, 163 - // }; 164 - 165 - // // Get track names 166 - // let mut track_no = 0; 167 - // let mut track_names = HashMap::<usize, String>::new(); 168 - // for track in midi.tracks.iter() { 169 - // track_no += 1; 170 - // let mut track_name = String::new(); 171 - // for event in track { 172 - // match event.kind { 173 - // TrackEventKind::Meta(MetaMessage::TrackName(name_bytes)) => { 174 - // track_name = String::from_utf8(name_bytes.to_vec()).unwrap_or_default(); 175 - // } 176 - // _ => {} 177 - // } 178 - // } 179 - // let track_name = if !track_name.is_empty() { 180 - // track_name 181 - // } else { 182 - // format!("Track #{}", track_no) 183 - // }; 184 - // if !stems.contains_key(&track_name) { 185 - // println!( 186 - // "MIDI track {} has no corresponding audio stem, skipping", 187 - // track_name 188 - // ); 189 - // } 190 - // track_names.insert(track_no, track_name); 191 - // } 192 - 193 - // // Convert ticks to absolute 194 - // let mut track_no = 0; 195 - // for track in midi.tracks.iter() { 196 - // track_no += 1; 197 - // let mut absolute_tick = 0; 198 - // for event in track { 199 - // absolute_tick += event.delta.as_int(); 200 - // timeline 201 - // .entry(absolute_tick) 202 - // .or_default() 203 - // .insert(track_names[&track_no].clone(), *event); 204 - // } 205 - // } 206 - 207 - // // Convert ticks to ms 208 - // let mut absolute_tick_to_ms = HashMap::<u32, f32>::new(); 209 - // let mut last_tick = 0; 210 - // for (tick, tracks) in timeline.iter().sorted_by_key(|(tick, _)| *tick) { 211 - // for (_, event) in tracks { 212 - // match event.kind { 213 - // TrackEventKind::Meta(MetaMessage::Tempo(tempo)) => { 214 - // now_tempo = tempo.as_int() as f32; 215 - // } 216 - // _ => {} 217 - // } 218 - // } 219 - // let delta = tick - last_tick; 220 - // last_tick = *tick; 221 - // let delta_µs = now_tempo * delta as f32 / ticks_per_beat as f32; 222 - // now_ms += delta_µs / 1000.0; 223 - // absolute_tick_to_ms.insert(*tick, now_ms); 224 - // } 225 - 226 - // // Add notes 227 - // for (tick, tracks) in timeline.iter().sorted_by_key(|(tick, _)| *tick) { 228 - // for (track_name, event) in tracks { 229 - // match event.kind { 230 - // TrackEventKind::Midi { 231 - // channel: _, 232 - // message, 233 - // } => match message { 234 - // MidiMessage::NoteOn { key, vel } | MidiMessage::NoteOff { key, vel } => { 235 - // let note = Note { 236 - // tick: *tick, 237 - // pitch: key.as_int(), 238 - // velocity: if matches!(message, MidiMessage::NoteOff { .. }) { 239 - // 0 240 - // } else { 241 - // vel.as_int() 242 - // }, 243 - // }; 244 - // let stem_name: &str = stem_audio_to_midi 245 - // .get(&track_name.as_str()) 246 - // .unwrap_or(&track_name.as_str()); 247 - // if stems.contains_key(stem_name) { 248 - // stems 249 - // .get_mut(stem_name) 250 - // .unwrap() 251 - // .notes 252 - // .entry(absolute_tick_to_ms[tick] as usize) 253 - // .or_default() 254 - // .push(note); 255 - // } 256 - // } 257 - // _ => {} 258 - // }, 259 - // _ => {} 260 - // } 261 - // } 262 - // } 263 - 264 - // std::fs::write("stems.json", serde_json::to_vec(&stems).unwrap()); 265 - 266 - // for (name, stem) in &stems { 267 - // // Write loaded stem to a CBOR cache file 268 - // Stem::save_to_cbor(&stem, &Stem::cbor_path(stem.path.clone(), name.to_string())); 269 - // } 270 - 271 - // main_progress_bar.finish_and_clear(); 272 - 273 - // println!("Loaded {} stems", stems.len()); 274 - 275 - // Self { 276 - // audio_paths: audio.clone(), 277 - // markers, 278 - // bpm, 279 - // stems, 280 - // ..self 281 - // } 282 - // }
+14 -205
src/canvas.rs src/graphics/canvas.rs
··· 4 4 use std::{collections::HashMap, ops::Range, sync::Arc}; 5 5 6 6 use itertools::Itertools as _; 7 - use measure_time::{debug_time, info_time}; 7 + use measure_time::info_time; 8 8 use rand::Rng; 9 9 10 10 use crate::{ 11 11 fonts::{load_fonts, FontOptions}, 12 - layer::Layer, 13 - objects::Object, 14 - random_color, Angle, Color, ColorMapping, ColoredObject, Containable, Fill, Filter, 15 - LineSegment, ObjectSizes, Point, Region, 12 + geometry::region::Containable, 13 + Color, ColorMapping, ColoredObject, Fill, Filter, Layer, LineSegment, Object, ObjectSizes, 14 + Point, Region, 16 15 }; 17 16 18 17 #[derive(Debug, Clone)] ··· 33 32 pub world_region: Region, 34 33 35 34 /// Render cache for the SVG string. Prevents having to re-calculate a pixmap when the SVG hasn't changed. 36 - png_render_cache: Option<String>, 37 - fontdb: Option<Arc<usvg::fontdb::Database>>, 35 + pub(crate) png_render_cache: Option<String>, 36 + pub(crate) fontdb: Option<Arc<usvg::fontdb::Database>>, 38 37 } 39 38 40 39 impl Canvas { ··· 243 242 let hatchable = object.hatchable(); 244 243 objects.insert( 245 244 format!("{}#{}", name, i), 246 - object.color(self.random_fill(hatchable)), 245 + object.color(if hatchable { 246 + Fill::random_hatches(self.background) 247 + } else { 248 + Fill::random_solid(self.background) 249 + }), 247 250 ); 248 251 } 249 252 Layer { ··· 274 277 ColoredObject::from(( 275 278 object, 276 279 if rand::thread_rng().gen_bool(0.5) { 277 - Some(self.random_fill(hatchable)) 280 + Some(Fill::random_solid(self.background)) 278 281 } else { 279 282 None 280 283 }, ··· 415 418 ) 416 419 } 417 420 418 - pub fn random_fill(&self, hatchable: bool) -> Fill { 419 - if hatchable { 420 - if rand::thread_rng().gen_bool(0.75) { 421 - Fill::Solid(random_color(self.background)) 422 - } else { 423 - let hatch_size = rand::thread_rng().gen_range(5..=100) as f32 * 1e-2; 424 - Fill::Hatched( 425 - random_color(self.background), 426 - Angle(rand::thread_rng().gen_range(0.0..360.0)), 427 - hatch_size, 428 - // under a certain hatch size, we can't see the hatching if the ratio is not ½ 429 - if hatch_size < 8.0 { 430 - 0.5 431 - } else { 432 - rand::thread_rng().gen_range(1..=4) as f32 / 4.0 433 - }, 434 - ) 435 - } 436 - } else { 437 - Fill::Solid(random_color(self.background)) 438 - } 439 - } 440 - 441 421 pub fn clear(&mut self) { 442 422 self.layers.clear(); 443 423 self.remove_background() ··· 513 493 /// returns a list of all unique filters used throughout the canvas 514 494 /// used to only generate one definition per filter 515 495 /// 516 - fn unique_filters(&self) -> Vec<Filter> { 496 + pub fn unique_filters(&self) -> Vec<Filter> { 517 497 self.layers 518 498 .iter() 519 499 .flat_map(|layer| layer.objects.iter().flat_map(|(_, o)| o.filters.clone())) ··· 521 501 .collect() 522 502 } 523 503 524 - fn unique_pattern_fills(&self) -> Vec<Fill> { 504 + pub fn unique_pattern_fills(&self) -> Vec<Fill> { 525 505 self.layers 526 506 .iter() 527 507 .flat_map(|layer| layer.objects.iter().flat_map(|(_, o)| o.fill)) ··· 554 534 Object::Rectangle(region.start, region.end).color(Fill::Translucent(color, 0.25)), 555 535 ) 556 536 } 557 - 558 - pub fn render_to_svg(&mut self) -> anyhow::Result<String> { 559 - debug_time!("render_to_svg"); 560 - let background_color = self.background.unwrap_or_default(); 561 - let mut svg = svg::Document::new(); 562 - svg = svg.add( 563 - svg::node::element::Rectangle::new() 564 - .set("x", -(self.canvas_outter_padding as i32)) 565 - .set("y", -(self.canvas_outter_padding as i32)) 566 - .set("width", self.width()) 567 - .set("height", self.height()) 568 - .set("fill", background_color.render(&self.colormap)), 569 - ); 570 - 571 - for layer in self.layers.iter_mut().filter(|layer| !layer.hidden).rev() { 572 - svg = svg.add(layer.render(self.colormap.clone(), self.cell_size, layer.object_sizes)); 573 - } 574 - 575 - let mut defs = svg::node::element::Definitions::new(); 576 - for filter in self.unique_filters() { 577 - defs = defs.add(filter.definition()) 578 - } 579 - 580 - for pattern_fill in self.unique_pattern_fills() { 581 - if let Some(patterndef) = pattern_fill.pattern_definition(&self.colormap) { 582 - defs = defs.add(patterndef) 583 - } 584 - } 585 - 586 - let rendered = svg 587 - .add(defs) 588 - .set( 589 - "viewBox", 590 - format!( 591 - "{0} {0} {1} {2}", 592 - -(self.canvas_outter_padding as i32), 593 - self.width(), 594 - self.height() 595 - ), 596 - ) 597 - .set("width", self.width()) 598 - .set("height", self.height()) 599 - .to_string(); 600 - 601 - Ok(rendered) 602 - } 603 - 604 - pub fn svg_to_pixmap( 605 - &self, 606 - width: u32, 607 - height: u32, 608 - contents: &str, 609 - ) -> anyhow::Result<tiny_skia::Pixmap> { 610 - info_time!("svg_to_pixmap"); 611 - 612 - let mut pixmap = self.create_pixmap(width, height); 613 - 614 - let parsed_svg = &svg_to_usvg_tree(contents, &self.fontdb)?; 615 - 616 - self.usvg_tree_to_pixmap(width, height, pixmap.as_mut(), parsed_svg); 617 - 618 - Ok(pixmap) 619 - } 620 - 621 - pub fn render_to_pixmap_no_cache( 622 - &mut self, 623 - width: u32, 624 - height: u32, 625 - ) -> anyhow::Result<tiny_skia::Pixmap> { 626 - let svg_contents = self.render_to_svg()?; 627 - self.svg_to_pixmap(width, height, &svg_contents) 628 - } 629 - 630 - // Returns None if we had a render cache hit -- pixmap is in self.png_render_cache in that case 631 - pub fn render_to_pixmap( 632 - &mut self, 633 - width: u32, 634 - height: u32, 635 - ) -> anyhow::Result<Option<tiny_skia::Pixmap>> { 636 - info_time!("render_to_pixmap"); 637 - 638 - self.load_fonts()?; 639 - 640 - let new_svg_contents = self.render_to_svg()?; 641 - if let Some(cached_svg) = &self.png_render_cache { 642 - if *cached_svg == new_svg_contents { 643 - // TODO find a way to avoid .cloneing the pixmap 644 - return Ok(None); 645 - } 646 - } 647 - 648 - let pixmap = self.svg_to_pixmap(width, height, &new_svg_contents)?; 649 - 650 - self.png_render_cache = Some(new_svg_contents); 651 - 652 - Ok(Some(pixmap)) 653 - } 654 - 655 - pub fn pixmap_to_hwc_frame( 656 - &self, 657 - resolution: u32, 658 - pixmap: &tiny_skia::Pixmap, 659 - ) -> anyhow::Result<video_rs::Frame> { 660 - info_time!("pixmap_to_hwc_frame"); 661 - let (width, height) = self.resolution_to_size(resolution); 662 - let (width, height) = (width as usize, height as usize); 663 - let mut data = vec![0u8; height * width * 3]; 664 - 665 - data.par_chunks_exact_mut(3) 666 - .enumerate() 667 - .for_each(|(index, chunk)| { 668 - let x = index % width; 669 - let y = index / width; 670 - 671 - let pixel = pixmap 672 - .pixel(x as u32, y as u32) 673 - .unwrap_or_else(|| panic!("No pixel found at x, y = {x}, {y}")); 674 - 675 - chunk[0] = pixel.red(); 676 - chunk[1] = pixel.green(); 677 - chunk[2] = pixel.blue(); 678 - }); 679 - 680 - Ok(video_rs::Frame::from_shape_vec([height, width, 3], data)?) 681 - } 682 - 683 - pub fn render_to_hwc_frame(&mut self, resolution: u32) -> anyhow::Result<video_rs::Frame> { 684 - let (width, height) = self.resolution_to_size(resolution); 685 - let pixmap = self.render_to_pixmap_no_cache(width, height)?; 686 - self.pixmap_to_hwc_frame(resolution, &pixmap) 687 - } 688 - 689 - fn usvg_tree_to_pixmap( 690 - &self, 691 - width: u32, 692 - height: u32, 693 - mut pixmap_mut: tiny_skia::PixmapMut<'_>, 694 - parsed_svg: &resvg::usvg::Tree, 695 - ) { 696 - info_time!("usvg_tree_to_pixmap"); 697 - resvg::render( 698 - parsed_svg, 699 - tiny_skia::Transform::from_scale( 700 - width as f32 / self.width() as f32, 701 - height as f32 / self.height() as f32, 702 - ), 703 - &mut pixmap_mut, 704 - ); 705 - } 706 - 707 - fn create_pixmap(&self, width: u32, height: u32) -> tiny_skia::Pixmap { 708 - info_time!("create_pixmap"); 709 - tiny_skia::Pixmap::new(width, height).expect("Failed to create pixmap") 710 - } 711 - } 712 - 713 - fn svg_to_usvg_tree( 714 - svg: &str, 715 - fontdb: &Option<Arc<usvg::fontdb::Database>>, 716 - ) -> anyhow::Result<resvg::usvg::Tree> { 717 - info_time!("svg_to_usvg_tree"); 718 - Ok(resvg::usvg::Tree::from_str( 719 - svg, 720 - &match fontdb { 721 - Some(fontdb) => resvg::usvg::Options { 722 - fontdb: fontdb.clone(), 723 - ..Default::default() 724 - }, 725 - None => resvg::usvg::Options::default(), 726 - }, 727 - )?) 728 537 }
+2
src/cli.rs src/cli/mod.rs
··· 1 + pub mod ui; 2 + 1 3 use crate::{Canvas, ColorMapping}; 2 4 use docopt::Docopt; 3 5 use measure_time::info_time;
+116
src/cli/ui.rs
··· 1 + use console::Style; 2 + use indicatif::{ProgressBar, ProgressStyle}; 3 + use std::borrow::Cow; 4 + use std::sync::{Arc, Mutex}; 5 + use std::thread::{self, JoinHandle}; 6 + use std::time; 7 + 8 + pub const PROGRESS_BARS_STYLE: &str = 9 + "{prefix:>12.bold.cyan} [{bar:25}] {pos}/{len}: {msg} ({eta} left)"; 10 + 11 + pub struct Spinner { 12 + pub spinner: ProgressBar, 13 + pub finished: Arc<Mutex<bool>>, 14 + pub thread: JoinHandle<()>, 15 + } 16 + 17 + impl Spinner { 18 + pub fn start(verb: &'static str, message: &str) -> Self { 19 + let spinner = ProgressBar::new(0).with_style( 20 + ProgressStyle::with_template(&format_log_msg_cyan( 21 + verb, 22 + &(message.to_owned() + " {spinner:.cyan}"), 23 + )) 24 + .unwrap(), 25 + ); 26 + spinner.tick(); 27 + 28 + let thread_spinner = spinner.clone(); 29 + let finished = Arc::new(Mutex::new(false)); 30 + let thread_finished = Arc::clone(&finished); 31 + let spinner_thread = thread::spawn(move || { 32 + while !*thread_finished.lock().unwrap() { 33 + thread_spinner.tick(); 34 + thread::sleep(time::Duration::from_millis(100)); 35 + } 36 + thread_spinner.finish_and_clear(); 37 + }); 38 + 39 + Self { 40 + spinner: spinner.clone(), 41 + finished, 42 + thread: spinner_thread, 43 + } 44 + } 45 + 46 + pub fn end(self, message: &str) { 47 + self.spinner.finish_and_clear(); 48 + *self.finished.lock().unwrap() = true; 49 + self.thread.join().unwrap(); 50 + println!("{}", message); 51 + } 52 + } 53 + 54 + pub fn setup_progress_bar(total: u64, verb: &'static str) -> ProgressBar { 55 + indicatif::ProgressBar::new(total) 56 + .with_prefix(verb) 57 + .with_style( 58 + indicatif::ProgressStyle::with_template(PROGRESS_BARS_STYLE) 59 + .unwrap() 60 + .progress_chars("=> "), 61 + ) 62 + } 63 + 64 + pub trait Log { 65 + fn log(&self, verb: &'static str, message: &str); 66 + } 67 + 68 + pub fn format_log_msg(verb: &'static str, message: &str) -> String { 69 + let style = Style::new().bold().green(); 70 + format!("{} {}", style.apply_to(format!("{verb:>12}")), message) 71 + } 72 + 73 + pub fn format_log_msg_cyan(verb: &'static str, message: &str) -> String { 74 + let style = Style::new().bold().cyan(); 75 + format!("{} {}", style.apply_to(format!("{verb:>12}")), message) 76 + } 77 + 78 + impl Log for ProgressBar { 79 + fn log(&self, verb: &'static str, message: &str) { 80 + self.println(format_log_msg(verb, message)); 81 + } 82 + } 83 + 84 + impl Log for Option<&ProgressBar> { 85 + fn log(&self, verb: &'static str, message: &str) { 86 + if let Some(pb) = self { 87 + pb.println(format_log_msg(verb, message)); 88 + } 89 + } 90 + } 91 + 92 + pub trait MaybeProgressBar<'a> { 93 + fn set_message(&'a self, message: impl Into<Cow<'static, str>>); 94 + fn inc(&'a self, n: u64); 95 + fn println(&'a self, message: impl AsRef<str>); 96 + } 97 + 98 + impl<'a> MaybeProgressBar<'a> for Option<&'a ProgressBar> { 99 + fn set_message(&'a self, message: impl Into<Cow<'static, str>>) { 100 + if let Some(pb) = self { 101 + pb.set_message(message); 102 + } 103 + } 104 + 105 + fn inc(&'a self, n: u64) { 106 + if let Some(pb) = self { 107 + pb.inc(n); 108 + } 109 + } 110 + 111 + fn println(&'a self, message: impl AsRef<str>) { 112 + if let Some(pb) = self { 113 + pb.println(message); 114 + } 115 + } 116 + }
-28
src/color.rs src/graphics/color.rs
··· 5 5 path::PathBuf, 6 6 }; 7 7 8 - use rand::Rng; 9 8 use serde::Deserialize; 10 9 use strum::IntoEnumIterator; 11 10 use strum_macros::EnumIter; ··· 26 25 Cyan, 27 26 Pink, 28 27 Gray, 29 - } 30 - 31 - #[wasm_bindgen] 32 - pub fn random_color(except: Option<Color>) -> Color { 33 - let all = [ 34 - Color::Black, 35 - Color::White, 36 - Color::Red, 37 - Color::Green, 38 - Color::Blue, 39 - Color::Yellow, 40 - Color::Orange, 41 - Color::Purple, 42 - Color::Brown, 43 - Color::Cyan, 44 - Color::Pink, 45 - Color::Gray, 46 - ]; 47 - let candidates = all 48 - .iter() 49 - .filter(|c| match except { 50 - None => true, 51 - Some(color) => &&color != c, 52 - }) 53 - .collect::<Vec<_>>(); 54 - 55 - *candidates[rand::thread_rng().gen_range(0..candidates.len())] 56 28 } 57 29 58 30 pub fn all_colors() -> Vec<Color> {
+1 -72
src/fill.rs src/graphics/fill.rs
··· 1 - use crate::{Color, ColorMapping, RenderCSS}; 2 - 3 - /// Angle, stored in degrees 4 - #[derive(Debug, Clone, Copy, Default)] 5 - pub struct Angle(pub f32); 6 - 7 - impl Angle { 8 - pub const TURN: Self = Angle(360.0); 9 - 10 - pub fn degrees(&self) -> f32 { 11 - self.0 12 - } 13 - 14 - pub fn radians(&self) -> f32 { 15 - self.0 * std::f32::consts::PI / (Self::TURN.0 / 2.0) 16 - } 17 - 18 - pub fn turns(&self) -> f32 { 19 - self.0 / Self::TURN.0 20 - } 21 - 22 - pub fn without_turns(&self) -> Self { 23 - Self(self.0 % Self::TURN.0) 24 - } 25 - } 26 - 27 - impl std::ops::Sub for Angle { 28 - type Output = Angle; 29 - 30 - fn sub(self, rhs: Self) -> Self::Output { 31 - Angle(self.0 - rhs.0) 32 - } 33 - } 34 - 35 - impl std::fmt::Display for Angle { 36 - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 37 - write!(f, "{}deg", self.degrees()) 38 - } 39 - } 1 + use crate::{Angle, Color, ColorMapping}; 40 2 41 3 #[derive(Debug, Clone, Copy)] 42 4 pub enum Fill { ··· 74 36 75 37 fn bottom_up_hatches(color: Color, thickness: f32, spacing: f32) -> Self { 76 38 Some(Fill::bottom_up_hatches(color, thickness, spacing)) 77 - } 78 - } 79 - 80 - impl RenderCSS for Fill { 81 - fn render_fill_css(&self, colormap: &ColorMapping) -> String { 82 - match self { 83 - Fill::Solid(color) => { 84 - format!("fill: {};", color.render(colormap)) 85 - } 86 - Fill::Translucent(color, opacity) => { 87 - format!("fill: {}; opacity: {};", color.render(colormap), opacity) 88 - } 89 - Fill::Dotted(..) | Fill::Hatched(..) => { 90 - format!("fill: url(#{});", self.pattern_id()) 91 - } 92 - } 93 - } 94 - 95 - fn render_stroke_css(&self, colormap: &ColorMapping) -> String { 96 - match self { 97 - Fill::Solid(color) => { 98 - format!("stroke: {}; fill: transparent;", color.render(colormap)) 99 - } 100 - Fill::Translucent(color, opacity) => { 101 - format!( 102 - "stroke: {}; opacity: {}; fill: transparent;", 103 - color.render(colormap), 104 - opacity 105 - ) 106 - } 107 - Fill::Dotted(..) => unimplemented!(), 108 - Fill::Hatched(..) => unimplemented!(), 109 - } 110 39 } 111 40 } 112 41
-157
src/filter.rs
··· 1 - use std::hash::Hash; 2 - 3 - use wasm_bindgen::prelude::*; 4 - 5 - use crate::RenderCSS; 6 - 7 - #[wasm_bindgen] 8 - #[derive(Debug, Clone, Copy, PartialEq)] 9 - pub enum FilterType { 10 - Glow, 11 - NaturalShadow, 12 - Saturation, 13 - } 14 - 15 - #[wasm_bindgen] 16 - #[derive(Debug, Clone, Copy)] 17 - pub struct Filter { 18 - pub kind: FilterType, 19 - pub parameter: f32, 20 - } 21 - 22 - #[wasm_bindgen] 23 - impl Filter { 24 - pub fn name(&self) -> String { 25 - match self.kind { 26 - FilterType::Glow => "glow", 27 - FilterType::NaturalShadow => "natural-shadow-filter", 28 - FilterType::Saturation => "saturation", 29 - } 30 - .to_owned() 31 - } 32 - 33 - pub fn glow(intensity: f32) -> Self { 34 - Self { 35 - kind: FilterType::Glow, 36 - parameter: intensity, 37 - } 38 - } 39 - 40 - pub fn id(&self) -> String { 41 - format!( 42 - "filter-{}-{}", 43 - self.name(), 44 - self.parameter.to_string().replace('.', "_") 45 - ) 46 - } 47 - } 48 - 49 - impl Filter { 50 - pub fn definition(&self) -> svg::node::element::Filter { 51 - match self.kind { 52 - FilterType::Glow => { 53 - // format!( 54 - // r#" 55 - // <filter id="glow"> 56 - // <feGaussianBlur stdDeviation="{}" result="coloredBlur"/> 57 - // <feMerge> 58 - // <feMergeNode in="coloredBlur"/> 59 - // <feMergeNode in="SourceGraphic"/> 60 - // </feMerge> 61 - // </filter> 62 - // "#, 63 - // 2.5 64 - // ) // TODO parameterize stdDeviation 65 - svg::node::element::Filter::new() 66 - .add( 67 - // TODO parameterize stdDeviation 68 - svg::node::element::FilterEffectGaussianBlur::new() 69 - .set("stdDeviation", self.parameter) 70 - .set("result", "coloredBlur"), 71 - ) 72 - .add( 73 - svg::node::element::FilterEffectMerge::new() 74 - .add( 75 - svg::node::element::FilterEffectMergeNode::new() 76 - .set("in", "coloredBlur"), 77 - ) 78 - .add( 79 - svg::node::element::FilterEffectMergeNode::new() 80 - .set("in", "SourceGraphic"), 81 - ), 82 - ) 83 - } 84 - FilterType::NaturalShadow => { 85 - /* 86 - <filter id="natural-shadow-filter" x="0" y="0" width="2" height="2"> 87 - <feOffset in="SourceGraphic" dx="3" dy="3" /> 88 - <feGaussianBlur stdDeviation="12" result="blur" /> 89 - <feMerge> 90 - <feMergeNode in="blur" /> 91 - <feMergeNode in="SourceGraphic" /> 92 - </feMerge> 93 - </filter> 94 - */ 95 - svg::node::element::Filter::new() 96 - .add( 97 - svg::node::element::FilterEffectOffset::new() 98 - .set("in", "SourceGraphic") 99 - .set("dx", self.parameter) 100 - .set("dy", self.parameter), 101 - ) 102 - .add( 103 - svg::node::element::FilterEffectGaussianBlur::new() 104 - .set("stdDeviation", self.parameter * 4.0) 105 - .set("result", "blur"), 106 - ) 107 - .add( 108 - svg::node::element::FilterEffectMerge::new() 109 - .add(svg::node::element::FilterEffectMergeNode::new().set("in", "blur")) 110 - .add( 111 - svg::node::element::FilterEffectMergeNode::new() 112 - .set("in", "SourceGraphic"), 113 - ), 114 - ) 115 - } 116 - FilterType::Saturation => { 117 - /* 118 - <filter id="saturation"> 119 - <feColorMatrix type="saturate" values="0.5"/> 120 - </filter> 121 - */ 122 - svg::node::element::Filter::new().add( 123 - svg::node::element::FilterEffectColorMatrix::new() 124 - .set("type", "saturate") 125 - .set("values", self.parameter), 126 - ) 127 - } 128 - } 129 - .set("id", self.id()) 130 - .set("filterUnit", "userSpaceOnUse") 131 - } 132 - } 133 - 134 - impl RenderCSS for Filter { 135 - fn render_fill_css(&self, _colormap: &crate::ColorMapping) -> String { 136 - format!("filter: url(#{}); overflow: visible;", self.id()) 137 - } 138 - 139 - fn render_stroke_css(&self, colormap: &crate::ColorMapping) -> String { 140 - self.render_fill_css(colormap) 141 - } 142 - } 143 - 144 - impl PartialEq for Filter { 145 - fn eq(&self, other: &Self) -> bool { 146 - // TODO use way less restrictive epsilon 147 - self.kind == other.kind && (self.parameter - other.parameter).abs() < f32::EPSILON 148 - } 149 - } 150 - 151 - impl Hash for Filter { 152 - fn hash<H: std::hash::Hasher>(&self, state: &mut H) { 153 - self.id().hash(state) 154 - } 155 - } 156 - 157 - impl Eq for Filter {}
src/fonts.rs src/rendering/fonts.rs
-56
src/from_flp.rs
··· 1 - use anyhow::Result; 2 - use serde::Deserialize; 3 - use std::{collections::HashMap, path::PathBuf}; 4 - 5 - #[derive(Debug, Deserialize)] 6 - pub struct FLStudioProject { 7 - pub info: FLStudioProjectMetadata, 8 - pub arrangements: HashMap<String, HashMap<String, ArrangementTrack>>, 9 - } 10 - 11 - #[derive(Debug, Deserialize)] 12 - pub struct FLStudioProjectMetadata { 13 - pub name: String, 14 - pub bpm: f32, 15 - } 16 - 17 - // #[derive(Debug, Deserialize)] 18 - // pub struct ArrangementTrack { 19 - // pub name: String, 20 - // pub clips: HashMap<u32, TrackClip>, 21 - // } 22 - 23 - type ArrangementTrack = HashMap<u32, TrackClip>; 24 - 25 - #[derive(Debug, Deserialize)] 26 - pub struct TrackClip { 27 - pub length: u32, 28 - pub name: String, 29 - pub data: TrackClipData, 30 - } 31 - 32 - #[derive(Debug, Deserialize, Default)] 33 - #[serde(default)] 34 - pub struct TrackClipData { 35 - pub notes: HashMap<u32, ClipNote>, 36 - pub values: HashMap<u32, f32>, 37 - pub length: u32, 38 - } 39 - 40 - #[derive(Debug, Deserialize)] 41 - pub struct ClipNote { 42 - pub key: NoteKey, 43 - pub pitch: u8, 44 - pub length: u32, 45 - pub velocity: u8, 46 - } 47 - 48 - /// A key for a note in a clip, in the "C5" notation 49 - type NoteKey = String; 50 - 51 - impl FLStudioProject { 52 - pub fn from_json(filepath: &PathBuf) -> Result<FLStudioProject> { 53 - let contents = std::fs::read_to_string(filepath)?; 54 - Ok(serde_json::from_str(&contents)?) 55 - } 56 - }
+39
src/geometry/angle.rs
··· 1 + 2 + /// Angle, stored in degrees 3 + #[derive(Debug, Clone, Copy, Default)] 4 + pub struct Angle(pub f32); 5 + 6 + impl Angle { 7 + pub const TURN: Self = Angle(360.0); 8 + 9 + pub fn degrees(&self) -> f32 { 10 + self.0 11 + } 12 + 13 + pub fn radians(&self) -> f32 { 14 + // tau better than pi, haters gonna hate <3 15 + self.0 * std::f32::consts::TAU / (Self::TURN.0 / 4.0) 16 + } 17 + 18 + pub fn turns(&self) -> f32 { 19 + self.0 / Self::TURN.0 20 + } 21 + 22 + pub fn without_turns(&self) -> Self { 23 + Self(self.0 % Self::TURN.0) 24 + } 25 + } 26 + 27 + impl std::ops::Sub for Angle { 28 + type Output = Angle; 29 + 30 + fn sub(self, rhs: Self) -> Self::Output { 31 + Angle(self.0 - rhs.0) 32 + } 33 + } 34 + 35 + impl std::fmt::Display for Angle { 36 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 37 + write!(f, "{}deg", self.degrees()) 38 + } 39 + }
+7
src/geometry/mod.rs
··· 1 + pub mod angle; 2 + pub mod point; 3 + pub mod region; 4 + 5 + pub use angle::Angle; 6 + pub use point::Point; 7 + pub use region::{Containable, Region};
+61
src/graphics/filter.rs
··· 1 + use std::hash::Hash; 2 + 3 + use wasm_bindgen::prelude::*; 4 + 5 + 6 + #[wasm_bindgen] 7 + #[derive(Debug, Clone, Copy, PartialEq)] 8 + pub enum FilterType { 9 + Glow, 10 + NaturalShadow, 11 + Saturation, 12 + } 13 + 14 + #[wasm_bindgen] 15 + #[derive(Debug, Clone, Copy)] 16 + pub struct Filter { 17 + pub kind: FilterType, 18 + pub parameter: f32, 19 + } 20 + 21 + #[wasm_bindgen] 22 + impl Filter { 23 + pub fn name(&self) -> String { 24 + match self.kind { 25 + FilterType::Glow => "glow", 26 + FilterType::NaturalShadow => "natural-shadow-filter", 27 + FilterType::Saturation => "saturation", 28 + } 29 + .to_owned() 30 + } 31 + 32 + pub fn glow(intensity: f32) -> Self { 33 + Self { 34 + kind: FilterType::Glow, 35 + parameter: intensity, 36 + } 37 + } 38 + 39 + pub fn id(&self) -> String { 40 + format!( 41 + "filter-{}-{}", 42 + self.name(), 43 + self.parameter.to_string().replace('.', "_") 44 + ) 45 + } 46 + } 47 + 48 + impl PartialEq for Filter { 49 + fn eq(&self, other: &Self) -> bool { 50 + // TODO use way less restrictive epsilon 51 + self.kind == other.kind && (self.parameter - other.parameter).abs() < f32::EPSILON 52 + } 53 + } 54 + 55 + impl Hash for Filter { 56 + fn hash<H: std::hash::Hasher>(&self, state: &mut H) { 57 + self.id().hash(state) 58 + } 59 + } 60 + 61 + impl Eq for Filter {}
+15
src/graphics/mod.rs
··· 1 + pub mod canvas; 2 + pub mod color; 3 + pub mod fill; 4 + pub mod filter; 5 + pub mod layer; 6 + pub mod objects; 7 + pub mod transform; 8 + 9 + pub use color::{Color, ColorMapping}; 10 + pub use fill::Fill; 11 + pub use filter::{Filter, FilterType}; 12 + pub use layer::Layer; 13 + pub use objects::{ColoredObject, LineSegment, Object, ObjectSizes}; 14 + pub use transform::{Transformation, TransformationType}; 15 + pub use canvas::Canvas;
+231
src/graphics/objects.rs
··· 1 + use crate::{ColorMapping, Fill, Filter, Point, Region, Transformation}; 2 + use std::collections::HashMap; 3 + use wasm_bindgen::prelude::*; 4 + 5 + #[derive(Debug, Clone, PartialEq, Eq)] 6 + pub enum LineSegment { 7 + Straight(Point), 8 + InwardCurve(Point), 9 + OutwardCurve(Point), 10 + } 11 + 12 + #[derive(Debug, Clone)] 13 + pub enum Object { 14 + Polygon(Point, Vec<LineSegment>), 15 + Line(Point, Point, f32), 16 + CurveOutward(Point, Point, f32), 17 + CurveInward(Point, Point, f32), 18 + SmallCircle(Point), 19 + Dot(Point), 20 + BigCircle(Point), 21 + Text(Point, String, f32), 22 + CenteredText(Point, String, f32), 23 + // FittedText(Region, String), 24 + Rectangle(Point, Point), 25 + Image(Region, String), 26 + RawSVG(Box<dyn svg::Node>), 27 + // Tiling(Region, Box<Object>), 28 + } 29 + 30 + impl Object { 31 + pub fn color(self, fill: Fill) -> ColoredObject { 32 + ColoredObject::from((self, Some(fill))) 33 + } 34 + 35 + pub fn filter(self, filter: Filter) -> ColoredObject { 36 + ColoredObject::from((self, None)).filter(filter) 37 + } 38 + 39 + pub fn transform(self, transformation: Transformation) -> ColoredObject { 40 + ColoredObject::from((self, None)).transform(transformation) 41 + } 42 + } 43 + 44 + #[derive(Debug, Clone)] 45 + pub struct ColoredObject { 46 + pub object: Object, 47 + pub fill: Option<Fill>, 48 + pub filters: Vec<Filter>, 49 + pub transformations: Vec<Transformation>, 50 + } 51 + 52 + impl ColoredObject { 53 + pub fn filter(mut self, filter: Filter) -> Self { 54 + self.filters.push(filter); 55 + self 56 + } 57 + 58 + pub fn transform(mut self, transformation: Transformation) -> Self { 59 + self.transformations.push(transformation); 60 + self 61 + } 62 + 63 + pub fn clear_filters(&mut self) { 64 + self.filters.clear(); 65 + } 66 + } 67 + 68 + impl std::fmt::Display for ColoredObject { 69 + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 70 + let ColoredObject { 71 + object, 72 + fill, 73 + filters, 74 + transformations, 75 + } = self; 76 + 77 + if fill.is_some() { 78 + write!(f, "{:?} {:?}", fill.unwrap(), object)?; 79 + } else { 80 + write!(f, "transparent {:?}", object)?; 81 + } 82 + 83 + if !filters.is_empty() { 84 + write!(f, " with filters {:?}", filters)?; 85 + } 86 + 87 + if !transformations.is_empty() { 88 + write!(f, " with transformations {:?}", transformations)?; 89 + } 90 + 91 + Ok(()) 92 + } 93 + } 94 + 95 + impl From<Object> for ColoredObject { 96 + fn from(value: Object) -> Self { 97 + ColoredObject { 98 + object: value, 99 + fill: None, 100 + filters: vec![], 101 + transformations: vec![], 102 + } 103 + } 104 + } 105 + 106 + impl From<(Object, Option<Fill>)> for ColoredObject { 107 + fn from((object, fill): (Object, Option<Fill>)) -> Self { 108 + ColoredObject { 109 + object, 110 + fill, 111 + filters: vec![], 112 + transformations: vec![], 113 + } 114 + } 115 + } 116 + 117 + #[wasm_bindgen] 118 + #[derive(Debug, Clone, Copy)] 119 + pub struct ObjectSizes { 120 + pub empty_shape_stroke_width: f32, 121 + pub small_circle_radius: f32, 122 + pub dot_radius: f32, 123 + pub default_line_width: f32, 124 + } 125 + 126 + impl Default for ObjectSizes { 127 + fn default() -> Self { 128 + Self { 129 + empty_shape_stroke_width: 0.5, 130 + small_circle_radius: 5.0, 131 + dot_radius: 2.0, 132 + default_line_width: 2.0, 133 + } 134 + } 135 + } 136 + 137 + impl Object { 138 + pub fn translate(&mut self, dx: i32, dy: i32) { 139 + match self { 140 + Object::Polygon(start, lines) => { 141 + start.translate(dx, dy); 142 + for line in lines { 143 + match line { 144 + LineSegment::InwardCurve(anchor) 145 + | LineSegment::OutwardCurve(anchor) 146 + | LineSegment::Straight(anchor) => anchor.translate(dx, dy), 147 + } 148 + } 149 + } 150 + Object::Line(start, end, _) 151 + | Object::CurveInward(start, end, _) 152 + | Object::CurveOutward(start, end, _) 153 + | Object::Rectangle(start, end) => { 154 + start.translate(dx, dy); 155 + end.translate(dx, dy); 156 + } 157 + Object::Text(anchor, _, _) 158 + | Object::CenteredText(anchor, ..) 159 + | Object::Dot(anchor) 160 + | Object::SmallCircle(anchor) => anchor.translate(dx, dy), 161 + Object::BigCircle(center) => center.translate(dx, dy), 162 + Object::Image(region, ..) => region.translate(dx, dy), 163 + Object::RawSVG(_) => { 164 + unimplemented!() 165 + } 166 + } 167 + } 168 + 169 + pub fn translate_with(&mut self, delta: (i32, i32)) { 170 + self.translate(delta.0, delta.1) 171 + } 172 + 173 + pub fn teleport(&mut self, x: i32, y: i32) { 174 + let Point(current_x, current_y) = self.region().start; 175 + let delta_x = x - current_x as i32; 176 + let delta_y = y - current_y as i32; 177 + self.translate(delta_x, delta_y); 178 + } 179 + 180 + pub fn teleport_with(&mut self, position: (i32, i32)) { 181 + self.teleport(position.0, position.1) 182 + } 183 + 184 + pub fn region(&self) -> Region { 185 + match self { 186 + Object::Polygon(start, lines) => { 187 + let mut region: Region = (start, start).into(); 188 + for line in lines { 189 + match line { 190 + LineSegment::InwardCurve(anchor) 191 + | LineSegment::OutwardCurve(anchor) 192 + | LineSegment::Straight(anchor) => { 193 + // println!( 194 + // "extending region {} with {}", 195 + // region, 196 + // Region::from((start, anchor)) 197 + // ); 198 + region = *region.max(&(start, anchor).into()) 199 + } 200 + } 201 + } 202 + // println!("region for {:?} -> {}", self, region); 203 + region 204 + } 205 + Object::Line(start, end, _) 206 + | Object::CurveInward(start, end, _) 207 + | Object::CurveOutward(start, end, _) 208 + | Object::Rectangle(start, end) => (start, end).into(), 209 + Object::Text(anchor, _, _) 210 + | Object::CenteredText(anchor, ..) 211 + | Object::Dot(anchor) 212 + | Object::SmallCircle(anchor) => anchor.region(), 213 + Object::BigCircle(center) => center.region(), 214 + Object::Image(region, ..) => *region, 215 + Object::RawSVG(_) => { 216 + unimplemented!() 217 + } 218 + } 219 + } 220 + 221 + pub fn fillable(&self) -> bool { 222 + !matches!( 223 + self, 224 + Object::Line(..) | Object::CurveInward(..) | Object::CurveOutward(..) 225 + ) 226 + } 227 + 228 + pub fn hatchable(&self) -> bool { 229 + self.fillable() && !matches!(self, Object::Dot(..)) 230 + } 231 + }
+41
src/graphics/transform.rs
··· 1 + use wasm_bindgen::prelude::*; 2 + 3 + use slug::slugify; 4 + 5 + #[wasm_bindgen] 6 + #[derive(Debug, Clone, Copy, PartialEq)] 7 + pub enum TransformationType { 8 + Scale, 9 + Rotate, 10 + Skew, 11 + Matrix, 12 + } 13 + 14 + #[derive(Debug, Clone, Copy, PartialEq)] 15 + pub enum Transformation { 16 + Scale(f32, f32), 17 + Rotate(f32), 18 + Skew(f32, f32), 19 + Matrix(f32, f32, f32, f32, f32, f32), 20 + } 21 + 22 + impl Transformation { 23 + pub fn name(&self) -> String { 24 + match self { 25 + Transformation::Matrix(..) => "matrix", 26 + Transformation::Rotate(..) => "rotate", 27 + Transformation::Scale(..) => "scale", 28 + Transformation::Skew(..) => "skew", 29 + } 30 + .to_owned() 31 + } 32 + 33 + #[allow(non_snake_case)] 34 + pub fn ScaleUniform(scale: f32) -> Self { 35 + Transformation::Scale(scale, scale) 36 + } 37 + 38 + pub fn id(&self) -> String { 39 + slugify(format!("{:?}", self)) 40 + } 41 + }
-25
src/layer.rs src/graphics/layer.rs
··· 123 123 self.remove_object(name); 124 124 self.add_object(name, object); 125 125 } 126 - 127 - /// Render the layer to a SVG group element. 128 - pub fn render( 129 - &mut self, 130 - colormap: ColorMapping, 131 - cell_size: usize, 132 - object_sizes: ObjectSizes, 133 - ) -> svg::node::element::Group { 134 - if !DISABLE_CACHE { 135 - if let Some(cached_svg) = &self._render_cache { 136 - return cached_svg.clone(); 137 - } 138 - } 139 - 140 - let mut layer_group = svg::node::element::Group::new() 141 - .set("class", "layer") 142 - .set("data-layer", self.name.clone()); 143 - 144 - for (id, obj) in &self.objects { 145 - layer_group = layer_group.add(obj.render(cell_size, object_sizes, &colormap, id)); 146 - } 147 - 148 - self._render_cache = Some(layer_group.clone()); 149 - layer_group 150 - } 151 126 }
+13 -191
src/lib.rs
··· 1 1 #![allow(uncommon_codepoints)] 2 2 3 - pub mod animation; 4 - pub mod audio; 5 - pub mod canvas; 6 3 pub mod cli; 7 - pub mod color; 4 + pub use cli::ui; 8 5 pub mod examples; 9 - pub mod fill; 10 - pub mod filter; 11 - pub mod fonts; 12 - pub mod layer; 13 - pub mod midi; 14 - pub mod objects; 15 - pub mod point; 16 - pub mod region; 17 - pub mod sync; 18 - pub mod transform; 19 - pub mod ui; 6 + pub mod geometry; 7 + pub mod graphics; 8 + pub mod random; 9 + pub mod rendering; 10 + pub mod synchronization; 20 11 pub mod video; 21 - pub mod vst; 22 - pub mod web; 23 - pub use animation::*; 24 - use anyhow::Result; 25 - pub use audio::*; 26 - pub use canvas::*; 27 - pub use color::*; 28 - pub use fill::*; 29 - pub use filter::*; 30 - use itertools::Itertools; 31 - pub use layer::*; 32 - pub use midi::MidiSynchronizer; 33 - pub use objects::*; 34 - pub use point::*; 35 - pub use region::*; 36 - pub use sync::Syncable; 37 - pub use transform::*; 38 - pub use video::*; 39 - pub use vst::*; 40 - pub use web::log; 41 12 42 - use nanoid::nanoid; 43 - use std::fs::{self}; 44 - use std::path::PathBuf; 45 - use sync::SyncData; 46 - 47 - pub struct Context<'a, AdditionalContext = ()> { 48 - pub frame: usize, 49 - pub beat: usize, 50 - pub beat_fractional: f32, 51 - pub timestamp: String, 52 - pub ms: usize, 53 - pub bpm: usize, 54 - pub syncdata: &'a SyncData, 55 - pub audiofile: PathBuf, 56 - pub later_hooks: Vec<LaterHook<AdditionalContext>>, 57 - pub extra: AdditionalContext, 58 - pub duration_override: Option<usize>, 59 - } 60 - 61 - impl<C> Context<'_, C> { 62 - pub fn stem(&self, name: &str) -> StemAtInstant { 63 - let stems = &self.syncdata.stems; 64 - if !stems.contains_key(name) { 65 - panic!( 66 - "No stem named {:?} found. Available stems:\n{}\n", 67 - name, 68 - stems 69 - .keys() 70 - .sorted() 71 - .fold(String::new(), |acc, k| format!("{acc}\n\t{k}")) 72 - ); 73 - } 74 - StemAtInstant { 75 - amplitude: *stems[name].amplitude_db.get(self.ms).unwrap_or(&0.0), 76 - amplitude_max: stems[name].amplitude_max, 77 - velocity_max: stems[name] 78 - .notes 79 - .get(&self.ms) 80 - .iter() 81 - .map(|notes| notes.iter().map(|note| note.velocity).max().unwrap_or(0)) 82 - .max() 83 - .unwrap_or(0), 84 - duration: stems[name].duration_ms, 85 - notes: stems[name].notes.get(&self.ms).cloned().unwrap_or(vec![]), 86 - } 87 - } 88 - 89 - pub fn dump_syncdata(&self, to: PathBuf) -> Result<()> { 90 - Ok(serde_cbor::to_writer(fs::File::create(to)?, self.syncdata)?) 91 - } 92 - 93 - pub fn marker(&self) -> String { 94 - self.syncdata 95 - .markers 96 - .get(&self.ms) 97 - .unwrap_or(&"".to_string()) 98 - .to_string() 99 - } 100 - 101 - pub fn duration_ms(&self) -> usize { 102 - match self.duration_override { 103 - Some(duration) => duration, 104 - None => self 105 - .syncdata 106 - .stems 107 - .values() 108 - .map(|stem| stem.duration_ms) 109 - .max() 110 - .unwrap(), 111 - } 112 - } 113 - 114 - pub fn later_frames(&mut self, delay: usize, render_function: &'static LaterRenderFunction) { 115 - let current_frame = self.frame; 116 - 117 - self.later_hooks.insert( 118 - 0, 119 - LaterHook { 120 - once: true, 121 - when: Box::new(move |_, context, _previous_beat| { 122 - context.frame >= current_frame + delay 123 - }), 124 - render_function: Box::new(render_function), 125 - }, 126 - ); 127 - } 128 - 129 - pub fn later_ms(&mut self, delay: usize, render_function: &'static LaterRenderFunction) { 130 - let current_ms = self.ms; 131 - 132 - self.later_hooks.insert( 133 - 0, 134 - LaterHook { 135 - once: true, 136 - when: Box::new(move |_, context, _previous_beat| context.ms >= current_ms + delay), 137 - render_function: Box::new(render_function), 138 - }, 139 - ); 140 - } 141 - 142 - pub fn later_beats(&mut self, delay: f32, render_function: &'static LaterRenderFunction) { 143 - let current_beat = self.beat; 144 - 145 - self.later_hooks.insert( 146 - 0, 147 - LaterHook { 148 - once: true, 149 - when: Box::new(move |_, context, _previous_beat| { 150 - context.beat_fractional >= current_beat as f32 + delay 151 - }), 152 - render_function: Box::new(render_function), 153 - }, 154 - ); 155 - } 156 - 157 - /// duration is in milliseconds 158 - pub fn start_animation(&mut self, duration: usize, animation: Animation) { 159 - let start_ms = self.ms; 160 - let ms_range = start_ms..(start_ms + duration); 161 - 162 - self.later_hooks.push(LaterHook { 163 - once: false, 164 - when: Box::new(move |_, ctx, _| ms_range.contains(&ctx.ms)), 165 - render_function: Box::new(move |canvas, ms| { 166 - let t = (ms - start_ms) as f32 / duration as f32; 167 - (animation.update)(t, canvas, ms) 168 - }), 169 - }) 170 - } 171 - 172 - /// duration is in milliseconds 173 - pub fn animate(&mut self, duration: usize, f: &'static AnimationUpdateFunction) { 174 - self.start_animation( 175 - duration, 176 - Animation::new(format!("unnamed animation {}", nanoid!()), f), 177 - ); 178 - } 179 - 180 - pub fn animate_layer( 181 - &mut self, 182 - layer: &'static str, 183 - duration: usize, 184 - f: &'static LayerAnimationUpdateFunction, 185 - ) { 186 - let animation = Animation { 187 - name: format!("unnamed animation {}", nanoid!()), 188 - update: Box::new(move |progress, canvas, ms| { 189 - (f)(progress, canvas.layer(layer), ms)?; 190 - canvas.layer(layer).flush(); 191 - Ok(()) 192 - }), 193 - }; 194 - 195 - self.start_animation(duration, animation); 196 - } 197 - } 13 + pub use geometry::{Angle, Containable, Point, Region}; 14 + pub use graphics::{ 15 + Canvas, Color, ColorMapping, ColoredObject, Fill, Filter, FilterType, Layer, LineSegment, 16 + Object, ObjectSizes, Transformation, 17 + }; 18 + pub use rendering::{fonts, CSSRenderable, SVGAttributesRenderable, SVGRenderable}; 19 + pub use video::{animation, context, Animation, Video}; 198 20 199 21 trait Toggleable { 200 22 fn toggle(&mut self);
+21 -5
src/main.rs
··· 20 20 canvas = examples::title(); 21 21 22 22 if args.arg_file.ends_with(".svg") { 23 - std::fs::write(args.arg_file, canvas.render_to_svg()?).unwrap(); 23 + std::fs::write( 24 + args.arg_file, 25 + canvas 26 + .render_to_svg( 27 + canvas.colormap.clone(), 28 + canvas.cell_size, 29 + canvas.object_sizes, 30 + "", 31 + )? 32 + .to_string(), 33 + ) 34 + .unwrap(); 24 35 } else { 25 36 match canvas.render_to_png(&args.arg_file, args.flag_resolution.unwrap_or(1000), None) { 26 37 Ok(_) => println!("Image saved to {}", args.arg_file), ··· 52 63 "text", 53 64 Object::CenteredText( 54 65 center, 55 - format!( 56 - "{} #{} beat {}", 57 - ctx.timestamp, ctx.frame, ctx.beat_fractional 58 - ), 66 + format!("{}", ctx.timestamp), 59 67 30.0, 60 68 ) 61 69 .color(Fill::Solid(Color::White)), 70 + ); 71 + canvas.root().add_object( 72 + "beat", 73 + Object::CenteredText( 74 + center.translated(0, 3), 75 + format!("beat {}", ctx.beat), 76 + 30.0, 77 + ).color(Fill::Solid(Color::Cyan)), 62 78 ); 63 79 Ok(()) 64 80 })
+3 -2
src/midi.rs src/synchronization/midi.rs
··· 1 + use super::audio::{self, Stem}; 2 + use super::sync::{SyncData, Syncable}; 3 + use crate::ui::{Log, MaybeProgressBar}; 1 4 use indicatif::ProgressBar; 2 5 use itertools::Itertools; 3 6 use midly::{MetaMessage, MidiMessage, TrackEvent, TrackEventKind}; 4 7 use std::{collections::HashMap, fmt::Debug, path::PathBuf}; 5 - 6 - use crate::{audio, sync::SyncData, ui::Log as _, ui::MaybeProgressBar as _, Stem, Syncable}; 7 8 8 9 pub struct MidiSynchronizer { 9 10 pub midi_path: PathBuf,
-631
src/objects.rs
··· 1 - use std::collections::HashMap; 2 - 3 - use crate::{ColorMapping, Fill, Filter, Point, Region, Transformation}; 4 - use itertools::Itertools; 5 - use wasm_bindgen::prelude::*; 6 - 7 - #[derive(Debug, Clone, PartialEq, Eq)] 8 - pub enum LineSegment { 9 - Straight(Point), 10 - InwardCurve(Point), 11 - OutwardCurve(Point), 12 - } 13 - 14 - #[derive(Debug, Clone)] 15 - pub enum Object { 16 - Polygon(Point, Vec<LineSegment>), 17 - Line(Point, Point, f32), 18 - CurveOutward(Point, Point, f32), 19 - CurveInward(Point, Point, f32), 20 - SmallCircle(Point), 21 - Dot(Point), 22 - BigCircle(Point), 23 - Text(Point, String, f32), 24 - CenteredText(Point, String, f32), 25 - // FittedText(Region, String), 26 - Rectangle(Point, Point), 27 - Image(Region, String), 28 - RawSVG(Box<dyn svg::Node>), 29 - // Tiling(Region, Box<Object>), 30 - } 31 - 32 - impl Object { 33 - pub fn color(self, fill: Fill) -> ColoredObject { 34 - ColoredObject::from((self, Some(fill))) 35 - } 36 - 37 - pub fn filter(self, filter: Filter) -> ColoredObject { 38 - ColoredObject::from((self, None)).filter(filter) 39 - } 40 - 41 - pub fn transform(self, transformation: Transformation) -> ColoredObject { 42 - ColoredObject::from((self, None)).transform(transformation) 43 - } 44 - } 45 - 46 - #[derive(Debug, Clone)] 47 - pub struct ColoredObject { 48 - pub object: Object, 49 - pub fill: Option<Fill>, 50 - pub filters: Vec<Filter>, 51 - pub transformations: Vec<Transformation>, 52 - } 53 - 54 - impl ColoredObject { 55 - pub fn filter(mut self, filter: Filter) -> Self { 56 - self.filters.push(filter); 57 - self 58 - } 59 - 60 - pub fn transform(mut self, transformation: Transformation) -> Self { 61 - self.transformations.push(transformation); 62 - self 63 - } 64 - 65 - pub fn clear_filters(&mut self) { 66 - self.filters.clear(); 67 - } 68 - 69 - pub fn render( 70 - &self, 71 - cell_size: usize, 72 - object_sizes: ObjectSizes, 73 - colormap: &ColorMapping, 74 - id: &str, 75 - ) -> svg::node::element::Group { 76 - let mut group = self.object.render(cell_size, object_sizes, id); 77 - 78 - for (key, value) in self 79 - .transformations 80 - .render_attributes(colormap, !self.object.fillable()) 81 - { 82 - group = group.set(key, value); 83 - } 84 - 85 - let start = self.object.region().start.coords(cell_size); 86 - let (w, h) = ( 87 - self.object.region().width() * cell_size, 88 - self.object.region().height() * cell_size, 89 - ); 90 - 91 - group = group.set( 92 - "transform-origin", 93 - format!( 94 - "{} {}", 95 - start.0 + (w as f32 / 2.0), 96 - start.1 + (h as f32 / 2.0) 97 - ), 98 - ); 99 - 100 - let mut css = String::new(); 101 - if !matches!(self.object, Object::RawSVG(..)) { 102 - css = self.fill.render_css(colormap, !self.object.fillable()); 103 - } 104 - 105 - css += "transform-box: fill-box;"; 106 - 107 - css += self 108 - .filters 109 - .iter() 110 - .map(|f| f.render_fill_css(colormap)) 111 - .join(" ") 112 - .as_ref(); 113 - 114 - group.set("style", css) 115 - } 116 - } 117 - 118 - impl std::fmt::Display for ColoredObject { 119 - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 120 - let ColoredObject { 121 - object, 122 - fill, 123 - filters, 124 - transformations, 125 - } = self; 126 - 127 - if fill.is_some() { 128 - write!(f, "{:?} {:?}", fill.unwrap(), object)?; 129 - } else { 130 - write!(f, "transparent {:?}", object)?; 131 - } 132 - 133 - if !filters.is_empty() { 134 - write!(f, " with filters {:?}", filters)?; 135 - } 136 - 137 - if !transformations.is_empty() { 138 - write!(f, " with transformations {:?}", transformations)?; 139 - } 140 - 141 - Ok(()) 142 - } 143 - } 144 - 145 - impl From<Object> for ColoredObject { 146 - fn from(value: Object) -> Self { 147 - ColoredObject { 148 - object: value, 149 - fill: None, 150 - filters: vec![], 151 - transformations: vec![], 152 - } 153 - } 154 - } 155 - 156 - impl From<(Object, Option<Fill>)> for ColoredObject { 157 - fn from((object, fill): (Object, Option<Fill>)) -> Self { 158 - ColoredObject { 159 - object, 160 - fill, 161 - filters: vec![], 162 - transformations: vec![], 163 - } 164 - } 165 - } 166 - 167 - #[wasm_bindgen] 168 - #[derive(Debug, Clone, Copy)] 169 - pub struct ObjectSizes { 170 - pub empty_shape_stroke_width: f32, 171 - pub small_circle_radius: f32, 172 - pub dot_radius: f32, 173 - pub default_line_width: f32, 174 - } 175 - 176 - impl Default for ObjectSizes { 177 - fn default() -> Self { 178 - Self { 179 - empty_shape_stroke_width: 0.5, 180 - small_circle_radius: 5.0, 181 - dot_radius: 2.0, 182 - default_line_width: 2.0, 183 - } 184 - } 185 - } 186 - 187 - pub trait RenderAttributes { 188 - const MULTIPLE_VALUES_JOIN_BY: &'static str = ", "; 189 - 190 - fn render_fill_attribute(&self, colormap: &ColorMapping) -> HashMap<String, String>; 191 - fn render_stroke_attribute(&self, colormap: &ColorMapping) -> HashMap<String, String>; 192 - fn render_attributes( 193 - &self, 194 - colormap: &ColorMapping, 195 - fill_as_stroke_color: bool, 196 - ) -> HashMap<String, String> { 197 - if fill_as_stroke_color { 198 - self.render_stroke_attribute(colormap) 199 - } else { 200 - self.render_fill_attribute(colormap) 201 - } 202 - } 203 - } 204 - impl<T: RenderAttributes> RenderAttributes for Vec<T> { 205 - fn render_fill_attribute(&self, colormap: &ColorMapping) -> HashMap<String, String> { 206 - let mut attrs = HashMap::<String, String>::new(); 207 - for attrmap in self.iter().map(|v| v.render_fill_attribute(colormap)) { 208 - for (key, value) in attrmap { 209 - if attrs.contains_key(&key) { 210 - attrs.insert( 211 - key.clone(), 212 - format!("{}{}{}", attrs[&key], T::MULTIPLE_VALUES_JOIN_BY, value), 213 - ); 214 - } else { 215 - attrs.insert(key, value); 216 - } 217 - } 218 - } 219 - attrs 220 - } 221 - 222 - fn render_stroke_attribute(&self, colormap: &ColorMapping) -> HashMap<String, String> { 223 - let mut attrs = HashMap::<String, String>::new(); 224 - for attrmap in self.iter().map(|v| v.render_stroke_attribute(colormap)) { 225 - for (key, value) in attrmap { 226 - if attrs.contains_key(&key) { 227 - attrs.insert( 228 - key.clone(), 229 - format!("{}{}{}", attrs[&key], T::MULTIPLE_VALUES_JOIN_BY, value), 230 - ); 231 - } else { 232 - attrs.insert(key, value); 233 - } 234 - } 235 - } 236 - attrs 237 - } 238 - } 239 - 240 - pub trait RenderCSS { 241 - fn render_fill_css(&self, colormap: &ColorMapping) -> String; 242 - fn render_stroke_css(&self, colormap: &ColorMapping) -> String; 243 - fn render_css(&self, colormap: &ColorMapping, fill_as_stroke_color: bool) -> String { 244 - if fill_as_stroke_color { 245 - self.render_stroke_css(colormap) 246 - } else { 247 - self.render_fill_css(colormap) 248 - } 249 - } 250 - } 251 - 252 - impl<T: RenderCSS> RenderCSS for Option<T> { 253 - fn render_fill_css(&self, colormap: &ColorMapping) -> String { 254 - self.as_ref() 255 - .map(|v| v.render_fill_css(colormap)) 256 - .unwrap_or_default() 257 - } 258 - 259 - fn render_stroke_css(&self, colormap: &ColorMapping) -> String { 260 - self.as_ref() 261 - .map(|v| v.render_stroke_css(colormap)) 262 - .unwrap_or_default() 263 - } 264 - } 265 - 266 - impl Object { 267 - pub fn translate(&mut self, dx: i32, dy: i32) { 268 - match self { 269 - Object::Polygon(start, lines) => { 270 - start.translate(dx, dy); 271 - for line in lines { 272 - match line { 273 - LineSegment::InwardCurve(anchor) 274 - | LineSegment::OutwardCurve(anchor) 275 - | LineSegment::Straight(anchor) => anchor.translate(dx, dy), 276 - } 277 - } 278 - } 279 - Object::Line(start, end, _) 280 - | Object::CurveInward(start, end, _) 281 - | Object::CurveOutward(start, end, _) 282 - | Object::Rectangle(start, end) => { 283 - start.translate(dx, dy); 284 - end.translate(dx, dy); 285 - } 286 - Object::Text(anchor, _, _) 287 - | Object::CenteredText(anchor, ..) 288 - | Object::Dot(anchor) 289 - | Object::SmallCircle(anchor) => anchor.translate(dx, dy), 290 - Object::BigCircle(center) => center.translate(dx, dy), 291 - Object::Image(region, ..) => region.translate(dx, dy), 292 - Object::RawSVG(_) => { 293 - unimplemented!() 294 - } 295 - } 296 - } 297 - 298 - pub fn translate_with(&mut self, delta: (i32, i32)) { 299 - self.translate(delta.0, delta.1) 300 - } 301 - 302 - pub fn teleport(&mut self, x: i32, y: i32) { 303 - let Point(current_x, current_y) = self.region().start; 304 - let delta_x = x - current_x as i32; 305 - let delta_y = y - current_y as i32; 306 - self.translate(delta_x, delta_y); 307 - } 308 - 309 - pub fn teleport_with(&mut self, position: (i32, i32)) { 310 - self.teleport(position.0, position.1) 311 - } 312 - 313 - pub fn region(&self) -> Region { 314 - match self { 315 - Object::Polygon(start, lines) => { 316 - let mut region: Region = (start, start).into(); 317 - for line in lines { 318 - match line { 319 - LineSegment::InwardCurve(anchor) 320 - | LineSegment::OutwardCurve(anchor) 321 - | LineSegment::Straight(anchor) => { 322 - // println!( 323 - // "extending region {} with {}", 324 - // region, 325 - // Region::from((start, anchor)) 326 - // ); 327 - region = *region.max(&(start, anchor).into()) 328 - } 329 - } 330 - } 331 - // println!("region for {:?} -> {}", self, region); 332 - region 333 - } 334 - Object::Line(start, end, _) 335 - | Object::CurveInward(start, end, _) 336 - | Object::CurveOutward(start, end, _) 337 - | Object::Rectangle(start, end) => (start, end).into(), 338 - Object::Text(anchor, _, _) 339 - | Object::CenteredText(anchor, ..) 340 - | Object::Dot(anchor) 341 - | Object::SmallCircle(anchor) => anchor.region(), 342 - Object::BigCircle(center) => center.region(), 343 - Object::Image(region, ..) => *region, 344 - Object::RawSVG(_) => { 345 - unimplemented!() 346 - } 347 - } 348 - } 349 - } 350 - 351 - impl Object { 352 - pub fn fillable(&self) -> bool { 353 - !matches!( 354 - self, 355 - Object::Line(..) | Object::CurveInward(..) | Object::CurveOutward(..) 356 - ) 357 - } 358 - 359 - pub fn hatchable(&self) -> bool { 360 - self.fillable() && !matches!(self, Object::Dot(..)) 361 - } 362 - 363 - pub fn render( 364 - &self, 365 - cell_size: usize, 366 - object_sizes: ObjectSizes, 367 - id: &str, 368 - ) -> svg::node::element::Group { 369 - let group = svg::node::element::Group::new(); 370 - 371 - let rendered = match self { 372 - Object::Text(..) | Object::CenteredText(..) => self.render_text(cell_size), 373 - Object::Rectangle(..) => self.render_rectangle(cell_size), 374 - Object::Polygon(..) => self.render_polygon(cell_size), 375 - Object::Line(..) => self.render_line(cell_size), 376 - Object::CurveInward(..) | Object::CurveOutward(..) => self.render_curve(cell_size), 377 - Object::SmallCircle(..) => self.render_small_circle(cell_size, object_sizes), 378 - Object::Dot(..) => self.render_dot(cell_size, object_sizes), 379 - Object::BigCircle(..) => self.render_big_circle(cell_size), 380 - Object::Image(..) => self.render_image(cell_size), 381 - Object::RawSVG(..) => self.render_raw_svg(), 382 - }; 383 - 384 - group.set("data-object", id).add(rendered) 385 - } 386 - 387 - fn render_image(&self, cell_size: usize) -> Box<dyn svg::node::Node> { 388 - if let Object::Image(region, path) = self { 389 - let (x, y) = region.start.coords(cell_size); 390 - return Box::new( 391 - svg::node::element::Image::new() 392 - .set("x", x) 393 - .set("y", y) 394 - .set("width", region.width() * cell_size) 395 - .set("height", region.height() * cell_size) 396 - .set("href", path.clone()), 397 - ); 398 - } 399 - 400 - panic!("Expected Image, got {:?}", self); 401 - } 402 - 403 - fn render_raw_svg(&self) -> Box<dyn svg::node::Node> { 404 - if let Object::RawSVG(svg) = self { 405 - return svg.clone(); 406 - } 407 - 408 - panic!("Expected RawSVG, got {:?}", self); 409 - } 410 - 411 - fn render_text(&self, cell_size: usize) -> Box<dyn svg::node::Node> { 412 - if let Object::Text(position, content, font_size) 413 - | Object::CenteredText(position, content, font_size) = self 414 - { 415 - let centered = matches!(self, Object::CenteredText(..)); 416 - 417 - let coords = if centered { 418 - position.center_coords(cell_size) 419 - } else { 420 - position.coords(cell_size) 421 - }; 422 - 423 - let mut node = svg::node::element::Text::new(content.clone()) 424 - .set("x", coords.0) 425 - .set("y", coords.1) 426 - .set("font-size", format!("{}pt", font_size)) 427 - .set("font-family", "Inconsolata"); 428 - 429 - if centered { 430 - node = node 431 - .set("text-anchor", "middle") 432 - // FIXME does not work with imagemagick 433 - .set("dominant-baseline", "middle"); 434 - } else { 435 - // FIXME does not work with imagemagick 436 - // see https://legacy.imagemagick.org/discourse-server/viewtopic.php?t=31540 437 - node = node.set("dominant-baseline", "hanging") 438 - } 439 - 440 - return Box::new(node); 441 - } 442 - 443 - panic!("Expected Text, got {:?}", self); 444 - } 445 - 446 - // fn render_fitted_text(&self, cell_size: usize) -> Box<dyn svg:node::Node> { 447 - // if let Object::FittedText(region, content) = self { 448 - // let (x, y) = region.start.coords(cell_size); 449 - // let width = region.width() * cell_size as f32; 450 - // let height = region.height() * cell_size as f32; 451 - 452 - // return Box::new( 453 - // svg::node::element::Text::new(content.clone()) 454 - // .set("x", x) 455 - // .set("y", y) 456 - // .set("") 457 - // .set("font-size", format!("{}pt", 10.0)) 458 - // .set("font-family", "sans-serif"), 459 - // ); 460 - // } 461 - 462 - // panic!("Expected FittedText, got {:?}", self); 463 - // } 464 - 465 - fn render_rectangle(&self, cell_size: usize) -> Box<dyn svg::node::Node> { 466 - if let Object::Rectangle(start, end) = self { 467 - return Box::new( 468 - svg::node::element::Rectangle::new() 469 - .set("x", start.coords(cell_size).0) 470 - .set("y", start.coords(cell_size).1) 471 - .set("width", start.distances(end).0 * cell_size) 472 - .set("height", start.distances(end).1 * cell_size), 473 - ); 474 - } 475 - 476 - panic!("Expected Rectangle, got {:?}", self); 477 - } 478 - 479 - fn render_polygon(&self, cell_size: usize) -> Box<dyn svg::node::Node> { 480 - if let Object::Polygon(start, lines) = self { 481 - let mut path = svg::node::element::path::Data::new(); 482 - path = path.move_to(start.coords(cell_size)); 483 - for line in lines { 484 - path = match line { 485 - LineSegment::Straight(end) 486 - | LineSegment::InwardCurve(end) 487 - | LineSegment::OutwardCurve(end) => path.line_to(end.coords(cell_size)), 488 - }; 489 - } 490 - path = path.close(); 491 - return Box::new(svg::node::element::Path::new().set("d", path)); 492 - } 493 - 494 - panic!("Expected Polygon, got {:?}", self); 495 - } 496 - 497 - fn render_line(&self, cell_size: usize) -> Box<dyn svg::node::Node> { 498 - if let Object::Line(start, end, width) = self { 499 - return Box::new( 500 - svg::node::element::Line::new() 501 - .set("x1", start.coords(cell_size).0) 502 - .set("y1", start.coords(cell_size).1) 503 - .set("x2", end.coords(cell_size).0) 504 - .set("y2", end.coords(cell_size).1) 505 - .set("stroke-width", *width), 506 - ); 507 - } 508 - 509 - panic!("Expected Line, got {:?}", self); 510 - } 511 - 512 - fn render_curve(&self, cell_size: usize) -> Box<dyn svg::node::Node> { 513 - if let Object::CurveOutward(start, end, _) | Object::CurveInward(start, end, _) = self { 514 - let inward = matches!(self, Object::CurveInward(..)); 515 - 516 - let (start_x, start_y) = start.coords(cell_size); 517 - let (end_x, end_y) = end.coords(cell_size); 518 - 519 - let midpoint = ((start_x + end_x) / 2.0, (start_y + end_y) / 2.0); 520 - let start_from_midpoint = (start_x - midpoint.0, start_y - midpoint.1); 521 - let end_from_midpoint = (end_x - midpoint.0, end_y - midpoint.1); 522 - 523 - let control = { 524 - let relative = (end_x - start_x, end_y - start_y); 525 - if start_from_midpoint.0 * start_from_midpoint.1 > 0.0 526 - && end_from_midpoint.0 * end_from_midpoint.1 > 0.0 527 - { 528 - if inward { 529 - ( 530 - midpoint.0 + relative.0.abs() / 2.0, 531 - midpoint.1 - relative.1.abs() / 2.0, 532 - ) 533 - } else { 534 - ( 535 - midpoint.0 - relative.0.abs() / 2.0, 536 - midpoint.1 + relative.1.abs() / 2.0, 537 - ) 538 - } 539 - // diagonal line is going like this: / 540 - } else if start_from_midpoint.0 * start_from_midpoint.1 < 0.0 541 - && end_from_midpoint.0 * end_from_midpoint.1 < 0.0 542 - { 543 - if inward { 544 - ( 545 - midpoint.0 - relative.0.abs() / 2.0, 546 - midpoint.1 - relative.1.abs() / 2.0, 547 - ) 548 - } else { 549 - ( 550 - midpoint.0 + relative.0.abs() / 2.0, 551 - midpoint.1 + relative.1.abs() / 2.0, 552 - ) 553 - } 554 - // line is horizontal 555 - } else if start_y == end_y { 556 - ( 557 - midpoint.0, 558 - midpoint.1 + (if inward { -1.0 } else { 1.0 }) * relative.0.abs() / 2.0, 559 - ) 560 - // line is vertical 561 - } else if start_x == end_x { 562 - ( 563 - midpoint.0 + (if inward { -1.0 } else { 1.0 }) * relative.1.abs() / 2.0, 564 - midpoint.1, 565 - ) 566 - } else { 567 - unreachable!() 568 - } 569 - }; 570 - 571 - return Box::new( 572 - svg::node::element::Path::new().set( 573 - "d", 574 - svg::node::element::path::Data::new() 575 - .move_to(start.coords(cell_size)) 576 - .quadratic_curve_to((control, end.coords(cell_size))), 577 - ), 578 - ); 579 - } 580 - 581 - panic!("Expected Curve, got {:?}", self); 582 - } 583 - 584 - fn render_small_circle( 585 - &self, 586 - cell_size: usize, 587 - object_sizes: ObjectSizes, 588 - ) -> Box<dyn svg::node::Node> { 589 - if let Object::SmallCircle(center) = self { 590 - return Box::new( 591 - svg::node::element::Circle::new() 592 - .set("cx", center.coords(cell_size).0) 593 - .set("cy", center.coords(cell_size).1) 594 - .set("r", object_sizes.small_circle_radius), 595 - ); 596 - } 597 - 598 - panic!("Expected SmallCircle, got {:?}", self); 599 - } 600 - 601 - fn render_dot(&self, cell_size: usize, object_sizes: ObjectSizes) -> Box<dyn svg::node::Node> { 602 - if let Object::Dot(center) = self { 603 - return Box::new( 604 - svg::node::element::Circle::new() 605 - .set("cx", center.coords(cell_size).0) 606 - .set("cy", center.coords(cell_size).1) 607 - .set("r", object_sizes.dot_radius), 608 - ); 609 - } 610 - 611 - panic!("Expected Dot, got {:?}", self); 612 - } 613 - 614 - fn render_big_circle(&self, cell_size: usize) -> Box<dyn svg::node::Node> { 615 - if let Object::BigCircle(topleft) = self { 616 - let (cx, cy) = { 617 - let (x, y) = topleft.coords(cell_size); 618 - (x + cell_size as f32 / 2.0, y + cell_size as f32 / 2.0) 619 - }; 620 - 621 - return Box::new( 622 - svg::node::element::Circle::new() 623 - .set("cx", cx) 624 - .set("cy", cy) 625 - .set("r", cell_size / 2), 626 - ); 627 - } 628 - 629 - panic!("Expected BigCircle, got {:?}", self); 630 - } 631 - }
src/point.rs src/geometry/point.rs
src/random/canvas.rs

This is a binary file and will not be displayed.

+31
src/random/color.rs
··· 1 + use rand::Rng; 2 + use wasm_bindgen::prelude::*; 3 + 4 + use crate::Color; 5 + 6 + #[wasm_bindgen] 7 + pub fn random_color(except: Option<Color>) -> Color { 8 + let all = [ 9 + Color::Black, 10 + Color::White, 11 + Color::Red, 12 + Color::Green, 13 + Color::Blue, 14 + Color::Yellow, 15 + Color::Orange, 16 + Color::Purple, 17 + Color::Brown, 18 + Color::Cyan, 19 + Color::Pink, 20 + Color::Gray, 21 + ]; 22 + let candidates = all 23 + .iter() 24 + .filter(|c| match except { 25 + None => true, 26 + Some(color) => &&color != c, 27 + }) 28 + .collect::<Vec<_>>(); 29 + 30 + *candidates[rand::thread_rng().gen_range(0..candidates.len())] 31 + }
+24
src/random/fill.rs
··· 1 + use super::random_color; 2 + use crate::{Angle, Color, Fill}; 3 + use rand::Rng; 4 + 5 + impl Fill { 6 + pub fn random_solid(except: Option<Color>) -> Self { 7 + Fill::Solid(random_color(except)) 8 + } 9 + 10 + pub fn random_hatches(except: Option<Color>) -> Self { 11 + let hatch_size = rand::thread_rng().gen_range(5..=100) as f32 * 1e-2; 12 + Fill::Hatched( 13 + random_color(except), 14 + Angle(rand::thread_rng().gen_range(0.0..360.0)), 15 + hatch_size, 16 + // under a certain hatch size, we can't see the hatching if the ratio is not ½ 17 + if hatch_size < 8.0 { 18 + 0.5 19 + } else { 20 + rand::thread_rng().gen_range(1..=4) as f32 / 4.0 21 + }, 22 + ) 23 + } 24 + }
src/random/layer.rs

This is a binary file and will not be displayed.

+13
src/random/mod.rs
··· 1 + pub mod canvas; 2 + pub mod color; 3 + pub mod fill; 4 + pub mod layer; 5 + pub mod objects; 6 + pub mod region; 7 + 8 + pub use canvas::*; 9 + pub use color::*; 10 + pub use fill::*; 11 + pub use layer::*; 12 + pub use objects::*; 13 + pub use region::*;
src/random/objects.rs

This is a binary file and will not be displayed.

src/random/region.rs

This is a binary file and will not be displayed.

src/region.rs src/geometry/region.rs
+202
src/rendering/canvas.rs
··· 1 + use super::renderable::SVGRenderable; 2 + use crate::graphics::canvas::Canvas; 3 + use measure_time::{debug_time, info_time}; 4 + use rayon::{ 5 + iter::{IndexedParallelIterator, ParallelIterator}, 6 + slice::ParallelSliceMut, 7 + }; 8 + use resvg::usvg; 9 + use std::sync::Arc; 10 + 11 + impl SVGRenderable for Canvas { 12 + fn render_to_svg( 13 + &self, 14 + _colormap: crate::ColorMapping, 15 + _cell_size: usize, 16 + _object_sizes: crate::graphics::objects::ObjectSizes, 17 + _id: &str, 18 + ) -> anyhow::Result<svg::node::element::Element> { 19 + debug_time!("render_to_svg"); 20 + let background_color = self.background.unwrap_or_default(); 21 + let mut svg = svg::Document::new(); 22 + svg = svg.add( 23 + svg::node::element::Rectangle::new() 24 + .set("x", -(self.canvas_outter_padding as i32)) 25 + .set("y", -(self.canvas_outter_padding as i32)) 26 + .set("width", self.width()) 27 + .set("height", self.height()) 28 + .set("fill", background_color.render(&self.colormap)), 29 + ); 30 + 31 + for layer in self.layers.iter().filter(|layer| !layer.hidden).rev() { 32 + svg = svg.add(layer.render_to_svg( 33 + self.colormap.clone(), 34 + self.cell_size, 35 + layer.object_sizes, 36 + "", 37 + )?); 38 + } 39 + 40 + let mut defs = svg::node::element::Definitions::new(); 41 + for filter in self.unique_filters() { 42 + defs = defs.add(filter.render_to_svg( 43 + self.colormap.clone(), 44 + self.cell_size, 45 + self.object_sizes, 46 + "", 47 + )?); 48 + } 49 + 50 + for pattern_fill in self.unique_pattern_fills() { 51 + if let Some(patterndef) = pattern_fill.pattern_definition(&self.colormap) { 52 + defs = defs.add(patterndef) 53 + } 54 + } 55 + 56 + Ok(svg 57 + .add(defs) 58 + .set( 59 + "viewBox", 60 + format!( 61 + "{0} {0} {1} {2}", 62 + -(self.canvas_outter_padding as i32), 63 + self.width(), 64 + self.height() 65 + ), 66 + ) 67 + .set("width", self.width()) 68 + .set("height", self.height()) 69 + .into()) 70 + } 71 + } 72 + 73 + impl Canvas { 74 + pub fn svg_to_pixmap( 75 + &self, 76 + width: u32, 77 + height: u32, 78 + contents: &str, 79 + ) -> anyhow::Result<tiny_skia::Pixmap> { 80 + info_time!("svg_to_pixmap"); 81 + 82 + let mut pixmap = self.create_pixmap(width, height); 83 + 84 + let parsed_svg = &svg_to_usvg_tree(contents, &self.fontdb)?; 85 + 86 + self.usvg_tree_to_pixmap(width, height, pixmap.as_mut(), parsed_svg); 87 + 88 + Ok(pixmap) 89 + } 90 + 91 + pub fn render_to_pixmap_no_cache( 92 + &mut self, 93 + width: u32, 94 + height: u32, 95 + ) -> anyhow::Result<tiny_skia::Pixmap> { 96 + let svg_contents = self 97 + .render_to_svg(self.colormap.clone(), self.cell_size, self.object_sizes, "")? 98 + .to_string(); 99 + self.svg_to_pixmap(width, height, &svg_contents) 100 + } 101 + 102 + // Returns None if we had a render cache hit -- pixmap is in self.png_render_cache in that case 103 + pub fn render_to_pixmap( 104 + &mut self, 105 + width: u32, 106 + height: u32, 107 + ) -> anyhow::Result<Option<tiny_skia::Pixmap>> { 108 + info_time!("render_to_pixmap"); 109 + 110 + self.load_fonts()?; 111 + 112 + let new_svg_contents = self 113 + .render_to_svg(self.colormap.clone(), self.cell_size, self.object_sizes, "")? 114 + .to_string(); 115 + if let Some(cached_svg) = &self.png_render_cache { 116 + if *cached_svg == new_svg_contents { 117 + // TODO find a way to avoid .cloneing the pixmap 118 + return Ok(None); 119 + } 120 + } 121 + 122 + let pixmap = self.svg_to_pixmap(width, height, &new_svg_contents)?; 123 + 124 + self.png_render_cache = Some(new_svg_contents); 125 + 126 + Ok(Some(pixmap)) 127 + } 128 + 129 + pub fn pixmap_to_hwc_frame( 130 + &self, 131 + resolution: u32, 132 + pixmap: &tiny_skia::Pixmap, 133 + ) -> anyhow::Result<video_rs::Frame> { 134 + info_time!("pixmap_to_hwc_frame"); 135 + let (width, height) = self.resolution_to_size(resolution); 136 + let (width, height) = (width as usize, height as usize); 137 + let mut data = vec![0u8; height * width * 3]; 138 + 139 + data.par_chunks_exact_mut(3) 140 + .enumerate() 141 + .for_each(|(index, chunk)| { 142 + let x = index % width; 143 + let y = index / width; 144 + 145 + let pixel = pixmap 146 + .pixel(x as u32, y as u32) 147 + .unwrap_or_else(|| panic!("No pixel found at x, y = {x}, {y}")); 148 + 149 + chunk[0] = pixel.red(); 150 + chunk[1] = pixel.green(); 151 + chunk[2] = pixel.blue(); 152 + }); 153 + 154 + Ok(video_rs::Frame::from_shape_vec([height, width, 3], data)?) 155 + } 156 + 157 + pub fn render_to_hwc_frame(&mut self, resolution: u32) -> anyhow::Result<video_rs::Frame> { 158 + let (width, height) = self.resolution_to_size(resolution); 159 + let pixmap = self.render_to_pixmap_no_cache(width, height)?; 160 + self.pixmap_to_hwc_frame(resolution, &pixmap) 161 + } 162 + 163 + fn usvg_tree_to_pixmap( 164 + &self, 165 + width: u32, 166 + height: u32, 167 + mut pixmap_mut: tiny_skia::PixmapMut<'_>, 168 + parsed_svg: &resvg::usvg::Tree, 169 + ) { 170 + info_time!("usvg_tree_to_pixmap"); 171 + resvg::render( 172 + parsed_svg, 173 + tiny_skia::Transform::from_scale( 174 + width as f32 / self.width() as f32, 175 + height as f32 / self.height() as f32, 176 + ), 177 + &mut pixmap_mut, 178 + ); 179 + } 180 + 181 + fn create_pixmap(&self, width: u32, height: u32) -> tiny_skia::Pixmap { 182 + info_time!("create_pixmap"); 183 + tiny_skia::Pixmap::new(width, height).expect("Failed to create pixmap") 184 + } 185 + } 186 + 187 + fn svg_to_usvg_tree( 188 + svg: &str, 189 + fontdb: &Option<Arc<usvg::fontdb::Database>>, 190 + ) -> anyhow::Result<resvg::usvg::Tree> { 191 + info_time!("svg_to_usvg_tree"); 192 + Ok(resvg::usvg::Tree::from_str( 193 + svg, 194 + &match fontdb { 195 + Some(fontdb) => resvg::usvg::Options { 196 + fontdb: fontdb.clone(), 197 + ..Default::default() 198 + }, 199 + None => resvg::usvg::Options::default(), 200 + }, 201 + )?) 202 + }
+35
src/rendering/fill.rs
··· 1 + use super::CSSRenderable; 2 + use crate::{ColorMapping, Fill}; 3 + 4 + impl CSSRenderable for Fill { 5 + fn render_to_css_filled(&self, colormap: &ColorMapping) -> String { 6 + match self { 7 + Fill::Solid(color) => { 8 + format!("fill: {};", color.render(colormap)) 9 + } 10 + Fill::Translucent(color, opacity) => { 11 + format!("fill: {}; opacity: {};", color.render(colormap), opacity) 12 + } 13 + Fill::Dotted(..) | Fill::Hatched(..) => { 14 + format!("fill: url(#{});", self.pattern_id()) 15 + } 16 + } 17 + } 18 + 19 + fn render_to_css_stroked(&self, colormap: &ColorMapping) -> String { 20 + match self { 21 + Fill::Solid(color) => { 22 + format!("stroke: {}; fill: transparent;", color.render(colormap)) 23 + } 24 + Fill::Translucent(color, opacity) => { 25 + format!( 26 + "stroke: {}; opacity: {}; fill: transparent;", 27 + color.render(colormap), 28 + opacity 29 + ) 30 + } 31 + Fill::Dotted(..) => unimplemented!(), 32 + Fill::Hatched(..) => unimplemented!(), 33 + } 34 + } 35 + }
+110
src/rendering/filter.rs
··· 1 + use crate::{ColorMapping, Filter, FilterType}; 2 + 3 + use super::{renderable::SVGRenderable, CSSRenderable}; 4 + 5 + impl SVGRenderable for Filter { 6 + fn render_to_svg( 7 + &self, 8 + colormap: crate::ColorMapping, 9 + cell_size: usize, 10 + object_sizes: crate::graphics::objects::ObjectSizes, 11 + id: &str, 12 + ) -> anyhow::Result<svg::node::element::Element> { 13 + { 14 + Ok(match self.kind { 15 + FilterType::Glow => { 16 + // format!( 17 + // r#" 18 + // <filter id="glow"> 19 + // <feGaussianBlur stdDeviation="{}" result="coloredBlur"/> 20 + // <feMerge> 21 + // <feMergeNode in="coloredBlur"/> 22 + // <feMergeNode in="SourceGraphic"/> 23 + // </feMerge> 24 + // </filter> 25 + // "#, 26 + // 2.5 27 + // ) // TODO parameterize stdDeviation 28 + svg::node::element::Filter::new() 29 + .add( 30 + // TODO parameterize stdDeviation 31 + svg::node::element::FilterEffectGaussianBlur::new() 32 + .set("stdDeviation", self.parameter) 33 + .set("result", "coloredBlur"), 34 + ) 35 + .add( 36 + svg::node::element::FilterEffectMerge::new() 37 + .add( 38 + svg::node::element::FilterEffectMergeNode::new() 39 + .set("in", "coloredBlur"), 40 + ) 41 + .add( 42 + svg::node::element::FilterEffectMergeNode::new() 43 + .set("in", "SourceGraphic"), 44 + ), 45 + ) 46 + } 47 + FilterType::NaturalShadow => { 48 + /* 49 + <filter id="natural-shadow-filter" x="0" y="0" width="2" height="2"> 50 + <feOffset in="SourceGraphic" dx="3" dy="3" /> 51 + <feGaussianBlur stdDeviation="12" result="blur" /> 52 + <feMerge> 53 + <feMergeNode in="blur" /> 54 + <feMergeNode in="SourceGraphic" /> 55 + </feMerge> 56 + </filter> 57 + */ 58 + svg::node::element::Filter::new() 59 + .add( 60 + svg::node::element::FilterEffectOffset::new() 61 + .set("in", "SourceGraphic") 62 + .set("dx", self.parameter) 63 + .set("dy", self.parameter), 64 + ) 65 + .add( 66 + svg::node::element::FilterEffectGaussianBlur::new() 67 + .set("stdDeviation", self.parameter * 4.0) 68 + .set("result", "blur"), 69 + ) 70 + .add( 71 + svg::node::element::FilterEffectMerge::new() 72 + .add( 73 + svg::node::element::FilterEffectMergeNode::new() 74 + .set("in", "blur"), 75 + ) 76 + .add( 77 + svg::node::element::FilterEffectMergeNode::new() 78 + .set("in", "SourceGraphic"), 79 + ), 80 + ) 81 + } 82 + FilterType::Saturation => { 83 + /* 84 + <filter id="saturation"> 85 + <feColorMatrix type="saturate" values="0.5"/> 86 + </filter> 87 + */ 88 + svg::node::element::Filter::new().add( 89 + svg::node::element::FilterEffectColorMatrix::new() 90 + .set("type", "saturate") 91 + .set("values", self.parameter), 92 + ) 93 + } 94 + } 95 + .set("id", self.id()) 96 + .set("filterUnit", "userSpaceOnUse") 97 + .into()) 98 + } 99 + } 100 + } 101 + 102 + impl CSSRenderable for Filter { 103 + fn render_to_css_filled(&self, _colormap: &ColorMapping) -> String { 104 + format!("filter: url(#{}); overflow: visible;", self.id()) 105 + } 106 + 107 + fn render_to_css_stroked(&self, colormap: &ColorMapping) -> String { 108 + self.render_to_css_filled(colormap) 109 + } 110 + }
+34
src/rendering/layer.rs
··· 1 + use measure_time::debug_time; 2 + 3 + use crate::Layer; 4 + 5 + use super::renderable::SVGRenderable; 6 + 7 + static DISABLE_CACHE: bool = true; 8 + 9 + impl SVGRenderable for Layer { 10 + fn render_to_svg( 11 + &self, 12 + colormap: crate::ColorMapping, 13 + cell_size: usize, 14 + object_sizes: crate::graphics::objects::ObjectSizes, 15 + id: &str, 16 + ) -> anyhow::Result<svg::node::element::Element> { 17 + debug_time!("render_to_svg"); 18 + 19 + let mut layer_group = svg::node::element::Group::new() 20 + .set("class", "layer") 21 + .set("data-layer", self.name.clone()); 22 + 23 + for (object_id, obj) in &self.objects { 24 + layer_group = layer_group.add(obj.render_to_svg( 25 + colormap.clone(), 26 + cell_size, 27 + object_sizes, 28 + &vec![id, object_id].join("--"), 29 + )?); 30 + } 31 + 32 + Ok(layer_group.into()) 33 + } 34 + }
+10
src/rendering/mod.rs
··· 1 + pub mod canvas; 2 + pub mod fill; 3 + pub mod filter; 4 + pub mod fonts; 5 + pub mod layer; 6 + pub mod objects; 7 + pub mod renderable; 8 + pub mod transform; 9 + 10 + pub use renderable::{CSSRenderable, SVGAttributesRenderable, SVGRenderable};
+343
src/rendering/objects.rs
··· 1 + use itertools::Itertools; 2 + 3 + use crate::{ 4 + graphics::objects::{LineSegment, ObjectSizes}, 5 + ColoredObject, Object, 6 + }; 7 + 8 + use super::{renderable::SVGRenderable, CSSRenderable, SVGAttributesRenderable}; 9 + 10 + impl SVGRenderable for ColoredObject { 11 + fn render_to_svg( 12 + &self, 13 + colormap: crate::ColorMapping, 14 + cell_size: usize, 15 + object_sizes: crate::graphics::objects::ObjectSizes, 16 + id: &str, 17 + ) -> anyhow::Result<svg::node::element::Element> { 18 + let mut group = self 19 + .object 20 + .render_to_svg(colormap.clone(), cell_size, object_sizes, id)?; 21 + 22 + let attributes = group.get_attributes_mut(); 23 + 24 + for (key, value) in self.transformations.render_to_svg_attributes( 25 + colormap.clone(), 26 + cell_size, 27 + object_sizes, 28 + id, 29 + )? { 30 + attributes.insert(key, value.into()); 31 + } 32 + 33 + let start = self.object.region().start.coords(cell_size); 34 + let (w, h) = ( 35 + self.object.region().width() * cell_size, 36 + self.object.region().height() * cell_size, 37 + ); 38 + 39 + attributes.insert( 40 + "transform-origin".to_string(), 41 + format!( 42 + "{} {}", 43 + start.0 + (w as f32 / 2.0), 44 + start.1 + (h as f32 / 2.0) 45 + ) 46 + .into(), 47 + ); 48 + 49 + let mut css = String::new(); 50 + if !matches!(self.object, Object::RawSVG(..)) { 51 + css = self 52 + .fill 53 + .render_to_css(&colormap.clone(), !self.object.fillable()); 54 + } 55 + 56 + css += "transform-box: fill-box;"; 57 + 58 + css += self 59 + .filters 60 + .iter() 61 + .map(|f| f.render_to_css_filled(&colormap)) 62 + .join(" ") 63 + .as_ref(); 64 + 65 + attributes.insert("style".into(), css.into()); 66 + 67 + Ok(group) 68 + } 69 + } 70 + 71 + impl SVGRenderable for Object { 72 + fn render_to_svg( 73 + &self, 74 + _colormap: crate::ColorMapping, 75 + cell_size: usize, 76 + object_sizes: crate::graphics::objects::ObjectSizes, 77 + id: &str, 78 + ) -> anyhow::Result<svg::node::element::Element> { 79 + let group = svg::node::element::Group::new(); 80 + 81 + let rendered = match self { 82 + Object::Text(..) | Object::CenteredText(..) => self.render_text(cell_size), 83 + Object::Rectangle(..) => self.render_rectangle(cell_size), 84 + Object::Polygon(..) => self.render_polygon(cell_size), 85 + Object::Line(..) => self.render_line(cell_size), 86 + Object::CurveInward(..) | Object::CurveOutward(..) => self.render_curve(cell_size), 87 + Object::SmallCircle(..) => self.render_small_circle(cell_size, object_sizes), 88 + Object::Dot(..) => self.render_dot(cell_size, object_sizes), 89 + Object::BigCircle(..) => self.render_big_circle(cell_size), 90 + Object::Image(..) => self.render_image(cell_size), 91 + Object::RawSVG(..) => self.render_raw_svg(), 92 + }; 93 + 94 + Ok(group.set("data-object", id).add(rendered).into()) 95 + } 96 + } 97 + 98 + impl Object { 99 + fn render_image(&self, cell_size: usize) -> Box<dyn svg::node::Node> { 100 + if let Object::Image(region, path) = self { 101 + let (x, y) = region.start.coords(cell_size); 102 + return Box::new( 103 + svg::node::element::Image::new() 104 + .set("x", x) 105 + .set("y", y) 106 + .set("width", region.width() * cell_size) 107 + .set("height", region.height() * cell_size) 108 + .set("href", path.clone()), 109 + ); 110 + } 111 + 112 + panic!("Expected Image, got {:?}", self); 113 + } 114 + 115 + fn render_raw_svg(&self) -> Box<dyn svg::node::Node> { 116 + if let Object::RawSVG(svg) = self { 117 + return svg.clone(); 118 + } 119 + 120 + panic!("Expected RawSVG, got {:?}", self); 121 + } 122 + 123 + fn render_text(&self, cell_size: usize) -> Box<dyn svg::node::Node> { 124 + if let Object::Text(position, content, font_size) 125 + | Object::CenteredText(position, content, font_size) = self 126 + { 127 + let centered = matches!(self, Object::CenteredText(..)); 128 + 129 + let coords = if centered { 130 + position.center_coords(cell_size) 131 + } else { 132 + position.coords(cell_size) 133 + }; 134 + 135 + let mut node = svg::node::element::Text::new(content.clone()) 136 + .set("x", coords.0) 137 + .set("y", coords.1) 138 + .set("font-size", format!("{}pt", font_size)) 139 + .set("font-family", "Inconsolata"); 140 + 141 + if centered { 142 + node = node 143 + .set("text-anchor", "middle") 144 + // FIXME does not work with imagemagick 145 + .set("dominant-baseline", "middle"); 146 + } else { 147 + // FIXME does not work with imagemagick 148 + // see https://legacy.imagemagick.org/discourse-server/viewtopic.php?t=31540 149 + node = node.set("dominant-baseline", "hanging") 150 + } 151 + 152 + return Box::new(node); 153 + } 154 + 155 + panic!("Expected Text, got {:?}", self); 156 + } 157 + 158 + // fn render_fitted_text(&self, cell_size: usize) -> Box<dyn svg:node::Node> { 159 + // if let Object::FittedText(region, content) = self { 160 + // let (x, y) = region.start.coords(cell_size); 161 + // let width = region.width() * cell_size as f32; 162 + // let height = region.height() * cell_size as f32; 163 + 164 + // return Box::new( 165 + // svg::node::element::Text::new(content.clone()) 166 + // .set("x", x) 167 + // .set("y", y) 168 + // .set("") 169 + // .set("font-size", format!("{}pt", 10.0)) 170 + // .set("font-family", "sans-serif"), 171 + // ); 172 + // } 173 + 174 + // panic!("Expected FittedText, got {:?}", self); 175 + // } 176 + 177 + fn render_rectangle(&self, cell_size: usize) -> Box<dyn svg::node::Node> { 178 + if let Object::Rectangle(start, end) = self { 179 + return Box::new( 180 + svg::node::element::Rectangle::new() 181 + .set("x", start.coords(cell_size).0) 182 + .set("y", start.coords(cell_size).1) 183 + .set("width", start.distances(end).0 * cell_size) 184 + .set("height", start.distances(end).1 * cell_size), 185 + ); 186 + } 187 + 188 + panic!("Expected Rectangle, got {:?}", self); 189 + } 190 + 191 + fn render_polygon(&self, cell_size: usize) -> Box<dyn svg::node::Node> { 192 + if let Object::Polygon(start, lines) = self { 193 + let mut path = svg::node::element::path::Data::new(); 194 + path = path.move_to(start.coords(cell_size)); 195 + for line in lines { 196 + path = match line { 197 + LineSegment::Straight(end) 198 + | LineSegment::InwardCurve(end) 199 + | LineSegment::OutwardCurve(end) => path.line_to(end.coords(cell_size)), 200 + }; 201 + } 202 + path = path.close(); 203 + return Box::new(svg::node::element::Path::new().set("d", path)); 204 + } 205 + 206 + panic!("Expected Polygon, got {:?}", self); 207 + } 208 + 209 + fn render_line(&self, cell_size: usize) -> Box<dyn svg::node::Node> { 210 + if let Object::Line(start, end, width) = self { 211 + return Box::new( 212 + svg::node::element::Line::new() 213 + .set("x1", start.coords(cell_size).0) 214 + .set("y1", start.coords(cell_size).1) 215 + .set("x2", end.coords(cell_size).0) 216 + .set("y2", end.coords(cell_size).1) 217 + .set("stroke-width", *width), 218 + ); 219 + } 220 + 221 + panic!("Expected Line, got {:?}", self); 222 + } 223 + 224 + fn render_curve(&self, cell_size: usize) -> Box<dyn svg::node::Node> { 225 + if let Object::CurveOutward(start, end, _) | Object::CurveInward(start, end, _) = self { 226 + let inward = matches!(self, Object::CurveInward(..)); 227 + 228 + let (start_x, start_y) = start.coords(cell_size); 229 + let (end_x, end_y) = end.coords(cell_size); 230 + 231 + let midpoint = ((start_x + end_x) / 2.0, (start_y + end_y) / 2.0); 232 + let start_from_midpoint = (start_x - midpoint.0, start_y - midpoint.1); 233 + let end_from_midpoint = (end_x - midpoint.0, end_y - midpoint.1); 234 + 235 + let control = { 236 + let relative = (end_x - start_x, end_y - start_y); 237 + if start_from_midpoint.0 * start_from_midpoint.1 > 0.0 238 + && end_from_midpoint.0 * end_from_midpoint.1 > 0.0 239 + { 240 + if inward { 241 + ( 242 + midpoint.0 + relative.0.abs() / 2.0, 243 + midpoint.1 - relative.1.abs() / 2.0, 244 + ) 245 + } else { 246 + ( 247 + midpoint.0 - relative.0.abs() / 2.0, 248 + midpoint.1 + relative.1.abs() / 2.0, 249 + ) 250 + } 251 + // diagonal line is going like this: / 252 + } else if start_from_midpoint.0 * start_from_midpoint.1 < 0.0 253 + && end_from_midpoint.0 * end_from_midpoint.1 < 0.0 254 + { 255 + if inward { 256 + ( 257 + midpoint.0 - relative.0.abs() / 2.0, 258 + midpoint.1 - relative.1.abs() / 2.0, 259 + ) 260 + } else { 261 + ( 262 + midpoint.0 + relative.0.abs() / 2.0, 263 + midpoint.1 + relative.1.abs() / 2.0, 264 + ) 265 + } 266 + // line is horizontal 267 + } else if start_y == end_y { 268 + ( 269 + midpoint.0, 270 + midpoint.1 + (if inward { -1.0 } else { 1.0 }) * relative.0.abs() / 2.0, 271 + ) 272 + // line is vertical 273 + } else if start_x == end_x { 274 + ( 275 + midpoint.0 + (if inward { -1.0 } else { 1.0 }) * relative.1.abs() / 2.0, 276 + midpoint.1, 277 + ) 278 + } else { 279 + unreachable!() 280 + } 281 + }; 282 + 283 + return Box::new( 284 + svg::node::element::Path::new().set( 285 + "d", 286 + svg::node::element::path::Data::new() 287 + .move_to(start.coords(cell_size)) 288 + .quadratic_curve_to((control, end.coords(cell_size))), 289 + ), 290 + ); 291 + } 292 + 293 + panic!("Expected Curve, got {:?}", self); 294 + } 295 + 296 + fn render_small_circle( 297 + &self, 298 + cell_size: usize, 299 + object_sizes: ObjectSizes, 300 + ) -> Box<dyn svg::node::Node> { 301 + if let Object::SmallCircle(center) = self { 302 + return Box::new( 303 + svg::node::element::Circle::new() 304 + .set("cx", center.coords(cell_size).0) 305 + .set("cy", center.coords(cell_size).1) 306 + .set("r", object_sizes.small_circle_radius), 307 + ); 308 + } 309 + 310 + panic!("Expected SmallCircle, got {:?}", self); 311 + } 312 + 313 + fn render_dot(&self, cell_size: usize, object_sizes: ObjectSizes) -> Box<dyn svg::node::Node> { 314 + if let Object::Dot(center) = self { 315 + return Box::new( 316 + svg::node::element::Circle::new() 317 + .set("cx", center.coords(cell_size).0) 318 + .set("cy", center.coords(cell_size).1) 319 + .set("r", object_sizes.dot_radius), 320 + ); 321 + } 322 + 323 + panic!("Expected Dot, got {:?}", self); 324 + } 325 + 326 + fn render_big_circle(&self, cell_size: usize) -> Box<dyn svg::node::Node> { 327 + if let Object::BigCircle(topleft) = self { 328 + let (cx, cy) = { 329 + let (x, y) = topleft.coords(cell_size); 330 + (x + cell_size as f32 / 2.0, y + cell_size as f32 / 2.0) 331 + }; 332 + 333 + return Box::new( 334 + svg::node::element::Circle::new() 335 + .set("cx", cx) 336 + .set("cy", cy) 337 + .set("r", cell_size / 2), 338 + ); 339 + } 340 + 341 + panic!("Expected BigCircle, got {:?}", self); 342 + } 343 + }
+95
src/rendering/renderable.rs
··· 1 + use crate::{graphics::objects::ObjectSizes, ColorMapping}; 2 + use anyhow::Result; 3 + use itertools::Itertools; 4 + use std::collections::HashMap; 5 + 6 + /// Struct can be rendered as a SVG element 7 + pub trait SVGRenderable { 8 + fn render_to_svg( 9 + &self, 10 + colormap: ColorMapping, 11 + cell_size: usize, 12 + object_sizes: ObjectSizes, 13 + id: &str, 14 + ) -> Result<svg::node::element::Element>; 15 + } 16 + 17 + /// Struct can be rendered as attributes of a SVG element 18 + pub trait SVGAttributesRenderable { 19 + /// When merging multiple SVGAttributesRenderable, this string is used to join multiple values for the same key 20 + const MULTIPLE_VALUES_JOIN_BY: &'static str = ", "; 21 + 22 + fn render_to_svg_attributes( 23 + &self, 24 + colormap: ColorMapping, 25 + cell_size: usize, 26 + object_sizes: ObjectSizes, 27 + id: &str, 28 + ) -> Result<HashMap<String, String>>; 29 + } 30 + 31 + /// Struct can be rendered as a CSS ruleset (e.g. no selectors) 32 + pub trait CSSRenderable { 33 + fn render_to_css_filled(&self, colormap: &ColorMapping) -> String; 34 + fn render_to_css_stroked(&self, colormap: &ColorMapping) -> String; 35 + fn render_to_css(&self, colormap: &ColorMapping, fill_as_stroke_color: bool) -> String { 36 + if fill_as_stroke_color { 37 + self.render_to_css_stroked(colormap) 38 + } else { 39 + self.render_to_css_filled(colormap) 40 + } 41 + } 42 + } 43 + 44 + impl<T: CSSRenderable, V: Clone + IntoIterator<Item = T>> CSSRenderable for V { 45 + fn render_to_css_filled(&self, colormap: &ColorMapping) -> String { 46 + self.clone() 47 + .into_iter() 48 + .map(|v| v.render_to_css_filled(colormap)) 49 + .join("\n") 50 + } 51 + 52 + fn render_to_css_stroked(&self, colormap: &ColorMapping) -> String { 53 + self.clone() 54 + .into_iter() 55 + .map(|v| v.render_to_css_stroked(colormap)) 56 + .join("\n") 57 + } 58 + } 59 + 60 + // We get the Option<T> implementation for free since Option<T> implements IntoIterator, and it works: 61 + // None => empty iterator => empty hashmap, 62 + // Some(T) => iterator with one element => one hashmap, no merging. 63 + // I love Rust <3. 64 + 65 + impl<T, V> SVGAttributesRenderable for V 66 + where 67 + T: SVGAttributesRenderable, 68 + V: Clone + IntoIterator<Item = T>, 69 + { 70 + fn render_to_svg_attributes( 71 + &self, 72 + colormap: ColorMapping, 73 + cell_size: usize, 74 + object_sizes: ObjectSizes, 75 + id: &str, 76 + ) -> Result<HashMap<String, String>> { 77 + let mut attrs = HashMap::<String, String>::new(); 78 + for attrmap in self.clone().into_iter().map(|v| { 79 + v.render_to_svg_attributes(colormap.clone(), cell_size, object_sizes, id) 80 + .unwrap() 81 + }) { 82 + for (key, value) in attrmap { 83 + if attrs.contains_key(&key) { 84 + attrs.insert( 85 + key.clone(), 86 + format!("{}{}{}", attrs[&key], T::MULTIPLE_VALUES_JOIN_BY, value), 87 + ); 88 + } else { 89 + attrs.insert(key, value); 90 + } 91 + } 92 + } 93 + Ok(attrs) 94 + } 95 + }
+27
src/rendering/transform.rs
··· 1 + use super::SVGAttributesRenderable; 2 + use crate::{ColorMapping, ObjectSizes, Transformation}; 3 + use std::collections::HashMap; 4 + 5 + impl SVGAttributesRenderable for Transformation { 6 + const MULTIPLE_VALUES_JOIN_BY: &'static str = " "; 7 + 8 + fn render_to_svg_attributes( 9 + &self, 10 + _colormap: ColorMapping, 11 + _cell_size: usize, 12 + _object_sizes: ObjectSizes, 13 + _id: &str, 14 + ) -> anyhow::Result<HashMap<String, String>> { 15 + Ok(HashMap::from([( 16 + "transform".to_string(), 17 + match self { 18 + Transformation::Scale(x, y) => format!("scale({} {})", x, y), 19 + Transformation::Rotate(angle) => format!("rotate({})", angle), 20 + Transformation::Skew(x, y) => format!("skewX({}) skewY({})", x, y), 21 + Transformation::Matrix(a, b, c, d, e, f) => { 22 + format!("matrix({}, {}, {}, {}, {}, {})", a, b, c, d, e, f) 23 + } 24 + }, 25 + )])) 26 + } 27 + }
+2 -4
src/sync.rs src/synchronization/sync.rs
··· 1 - use std::collections::HashMap; 2 - 1 + use super::audio::Stem; 3 2 use serde::{Deserialize, Serialize}; 4 - 5 - use crate::Stem; 3 + use std::collections::HashMap; 6 4 7 5 pub type TimestampMS = usize; 8 6
+3
src/synchronization/mod.rs
··· 1 + pub mod midi; 2 + pub mod audio; 3 + pub mod sync;
-97
src/transform.rs
··· 1 - use std::collections::HashMap; 2 - 3 - use slug::slugify; 4 - use wasm_bindgen::prelude::*; 5 - 6 - use crate::RenderAttributes; 7 - 8 - #[wasm_bindgen] 9 - #[derive(Debug, Clone, Copy, PartialEq)] 10 - pub enum TransformationType { 11 - Scale, 12 - Rotate, 13 - Skew, 14 - Matrix, 15 - } 16 - 17 - #[wasm_bindgen(getter_with_clone)] 18 - #[derive(Debug, Clone)] 19 - pub struct TransformationWASM { 20 - pub kind: TransformationType, 21 - pub parameters: Vec<f32>, 22 - } 23 - 24 - #[derive(Debug, Clone, Copy, PartialEq)] 25 - pub enum Transformation { 26 - Scale(f32, f32), 27 - Rotate(f32), 28 - Skew(f32, f32), 29 - Matrix(f32, f32, f32, f32, f32, f32), 30 - } 31 - 32 - impl From<TransformationWASM> for Transformation { 33 - fn from(transformation: TransformationWASM) -> Self { 34 - match transformation.kind { 35 - TransformationType::Scale => { 36 - Transformation::Scale(transformation.parameters[0], transformation.parameters[1]) 37 - } 38 - TransformationType::Rotate => Transformation::Rotate(transformation.parameters[0]), 39 - TransformationType::Skew => { 40 - Transformation::Skew(transformation.parameters[0], transformation.parameters[1]) 41 - } 42 - TransformationType::Matrix => Transformation::Matrix( 43 - transformation.parameters[0], 44 - transformation.parameters[1], 45 - transformation.parameters[2], 46 - transformation.parameters[3], 47 - transformation.parameters[4], 48 - transformation.parameters[5], 49 - ), 50 - } 51 - } 52 - } 53 - 54 - impl Transformation { 55 - pub fn name(&self) -> String { 56 - match self { 57 - Transformation::Matrix(..) => "matrix", 58 - Transformation::Rotate(..) => "rotate", 59 - Transformation::Scale(..) => "scale", 60 - Transformation::Skew(..) => "skew", 61 - } 62 - .to_owned() 63 - } 64 - 65 - #[allow(non_snake_case)] 66 - pub fn ScaleUniform(scale: f32) -> Self { 67 - Transformation::Scale(scale, scale) 68 - } 69 - 70 - pub fn id(&self) -> String { 71 - slugify(format!("{:?}", self)) 72 - } 73 - } 74 - 75 - impl RenderAttributes for Transformation { 76 - const MULTIPLE_VALUES_JOIN_BY: &'static str = " "; 77 - 78 - fn render_fill_attribute(&self, _colormap: &crate::ColorMapping) -> HashMap<String, String> { 79 - let mut attrs = HashMap::new(); 80 - attrs.insert( 81 - "transform".to_string(), 82 - match self { 83 - Transformation::Scale(x, y) => format!("scale({} {})", x, y), 84 - Transformation::Rotate(angle) => format!("rotate({})", angle), 85 - Transformation::Skew(x, y) => format!("skewX({}) skewY({})", x, y), 86 - Transformation::Matrix(a, b, c, d, e, f) => { 87 - format!("matrix({}, {}, {}, {}, {}, {})", a, b, c, d, e, f) 88 - } 89 - }, 90 - ); 91 - attrs 92 - } 93 - 94 - fn render_stroke_attribute(&self, colormap: &crate::ColorMapping) -> HashMap<String, String> { 95 - self.render_fill_attribute(colormap) 96 - } 97 - }
-116
src/ui.rs
··· 1 - use console::Style; 2 - use indicatif::{ProgressBar, ProgressStyle}; 3 - use std::borrow::Cow; 4 - use std::sync::{Arc, Mutex}; 5 - use std::thread::{self, JoinHandle}; 6 - use std::time; 7 - 8 - pub const PROGRESS_BARS_STYLE: &str = 9 - "{prefix:>12.bold.cyan} [{bar:25}] {pos}/{len}: {msg} ({eta} left)"; 10 - 11 - pub struct Spinner { 12 - pub spinner: ProgressBar, 13 - pub finished: Arc<Mutex<bool>>, 14 - pub thread: JoinHandle<()>, 15 - } 16 - 17 - impl Spinner { 18 - pub fn start(verb: &'static str, message: &str) -> Self { 19 - let spinner = ProgressBar::new(0).with_style( 20 - ProgressStyle::with_template(&format_log_msg_cyan( 21 - verb, 22 - &(message.to_owned() + " {spinner:.cyan}"), 23 - )) 24 - .unwrap(), 25 - ); 26 - spinner.tick(); 27 - 28 - let thread_spinner = spinner.clone(); 29 - let finished = Arc::new(Mutex::new(false)); 30 - let thread_finished = Arc::clone(&finished); 31 - let spinner_thread = thread::spawn(move || { 32 - while !*thread_finished.lock().unwrap() { 33 - thread_spinner.tick(); 34 - thread::sleep(time::Duration::from_millis(100)); 35 - } 36 - thread_spinner.finish_and_clear(); 37 - }); 38 - 39 - Self { 40 - spinner: spinner.clone(), 41 - finished, 42 - thread: spinner_thread, 43 - } 44 - } 45 - 46 - pub fn end(self, message: &str) { 47 - self.spinner.finish_and_clear(); 48 - *self.finished.lock().unwrap() = true; 49 - self.thread.join().unwrap(); 50 - println!("{}", message); 51 - } 52 - } 53 - 54 - pub fn setup_progress_bar(total: u64, verb: &'static str) -> ProgressBar { 55 - indicatif::ProgressBar::new(total) 56 - .with_prefix(verb) 57 - .with_style( 58 - indicatif::ProgressStyle::with_template(PROGRESS_BARS_STYLE) 59 - .unwrap() 60 - .progress_chars("=> "), 61 - ) 62 - } 63 - 64 - pub trait Log { 65 - fn log(&self, verb: &'static str, message: &str); 66 - } 67 - 68 - pub fn format_log_msg(verb: &'static str, message: &str) -> String { 69 - let style = Style::new().bold().green(); 70 - format!("{} {}", style.apply_to(format!("{verb:>12}")), message) 71 - } 72 - 73 - pub fn format_log_msg_cyan(verb: &'static str, message: &str) -> String { 74 - let style = Style::new().bold().cyan(); 75 - format!("{} {}", style.apply_to(format!("{verb:>12}")), message) 76 - } 77 - 78 - impl Log for ProgressBar { 79 - fn log(&self, verb: &'static str, message: &str) { 80 - self.println(format_log_msg(verb, message)); 81 - } 82 - } 83 - 84 - impl Log for Option<&ProgressBar> { 85 - fn log(&self, verb: &'static str, message: &str) { 86 - if let Some(pb) = self { 87 - pb.println(format_log_msg(verb, message)); 88 - } 89 - } 90 - } 91 - 92 - pub trait MaybeProgressBar<'a> { 93 - fn set_message(&'a self, message: impl Into<Cow<'static, str>>); 94 - fn inc(&'a self, n: u64); 95 - fn println(&'a self, message: impl AsRef<str>); 96 - } 97 - 98 - impl<'a> MaybeProgressBar<'a> for Option<&'a ProgressBar> { 99 - fn set_message(&'a self, message: impl Into<Cow<'static, str>>) { 100 - if let Some(pb) = self { 101 - pb.set_message(message); 102 - } 103 - } 104 - 105 - fn inc(&'a self, n: u64) { 106 - if let Some(pb) = self { 107 - pb.inc(n); 108 - } 109 - } 110 - 111 - fn println(&'a self, message: impl AsRef<str>) { 112 - if let Some(pb) = self { 113 - pb.println(message); 114 - } 115 - } 116 - }
+26 -17
src/video.rs src/video/video.rs
··· 1 + extern crate ffmpeg_next as ffmpeg; 2 + use super::animation::LayerAnimationUpdateFunction; 3 + use super::context::Context; 4 + use crate::synchronization::audio::MusicalDurationUnit; 5 + use crate::synchronization::midi::MidiSynchronizer; 6 + use crate::synchronization::sync::{SyncData, Syncable}; 7 + use crate::ui::{self, setup_progress_bar, Log as _}; 8 + use crate::{Canvas, ColoredObject, SVGRenderable}; 9 + use anyhow::Result; 10 + use chrono::{DateTime, NaiveDateTime}; 11 + use indicatif::{ProgressBar, ProgressIterator}; 12 + use itertools::Itertools; 13 + use measure_time::{debug_time, info_time}; 14 + use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; 1 15 use std::str::FromStr; 2 16 use std::sync::{Arc, Mutex}; 3 17 use std::{ ··· 6 20 panic, 7 21 path::{Path, PathBuf}, 8 22 }; 9 - 10 - extern crate ffmpeg_next as ffmpeg; 11 - use anyhow::Result; 12 - use chrono::{DateTime, NaiveDateTime}; 13 - use indicatif::{ProgressBar, ProgressIterator}; 14 - use itertools::Itertools; 15 - use measure_time::{debug_time, info_time}; 16 - use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; 17 23 use video_rs::Time; 18 - 19 - use crate::{ 20 - sync::SyncData, 21 - ui::{self, setup_progress_bar, Log as _}, 22 - Canvas, ColoredObject, Context, LayerAnimationUpdateFunction, MidiSynchronizer, 23 - MusicalDurationUnit, Syncable, 24 - }; 25 24 26 25 pub type BeatNumber = usize; 27 26 pub type FrameNumber = usize; ··· 458 457 .expect("No audio sync data provided. Use .sync_audio_with() to load a MIDI file, or provide a duration override.") 459 458 } 460 459 461 - // Saves PNG frames to disk. Returns number of frames written. 462 460 pub fn render_frames(&mut self) -> Result<usize> { 463 461 let mut written_frames_count: usize = 0; 464 462 let mut context = Context { ··· 558 556 debug_time!("compute_frame"); 559 557 frames_to_encode.push(( 560 558 Time::from_secs_f64(context.ms as f64 * 1e-3), 561 - canvas.render_to_svg()?, 559 + canvas 560 + .render_to_svg( 561 + canvas.colormap.clone(), 562 + canvas.cell_size, 563 + canvas.object_sizes, 564 + "", 565 + )? 566 + .to_string(), 562 567 )); 563 568 564 569 written_frames_count += 1; ··· 614 619 .unwrap() 615 620 .encode(&frame, time) 616 621 .expect("Failed to encode frame"); 622 + 623 + self.progress_bar.inc(1); 617 624 } 625 + 626 + self.progress_bar.finish(); 618 627 619 628 Ok(written_frames_count) 620 629 }
+161
src/video/context.rs
··· 1 + use super::animation::{AnimationUpdateFunction, LayerAnimationUpdateFunction}; 2 + use super::video::{LaterHook, LaterRenderFunction}; 3 + use super::Animation; 4 + use crate::synchronization::audio::StemAtInstant; 5 + use crate::synchronization::sync::SyncData; 6 + use itertools::Itertools; 7 + use nanoid::nanoid; 8 + use std::fs::{self}; 9 + use std::path::PathBuf; 10 + 11 + pub struct Context<'a, AdditionalContext = ()> { 12 + pub frame: usize, 13 + pub beat: usize, 14 + pub beat_fractional: f32, 15 + pub timestamp: String, 16 + pub ms: usize, 17 + pub bpm: usize, 18 + pub syncdata: &'a SyncData, 19 + pub audiofile: PathBuf, 20 + pub later_hooks: Vec<LaterHook<AdditionalContext>>, 21 + pub extra: AdditionalContext, 22 + pub duration_override: Option<usize>, 23 + } 24 + 25 + impl<C> Context<'_, C> { 26 + pub fn stem(&self, name: &str) -> StemAtInstant { 27 + let stems = &self.syncdata.stems; 28 + if !stems.contains_key(name) { 29 + panic!( 30 + "No stem named {:?} found. Available stems:\n{}\n", 31 + name, 32 + stems 33 + .keys() 34 + .sorted() 35 + .fold(String::new(), |acc, k| format!("{acc}\n\t{k}")) 36 + ); 37 + } 38 + StemAtInstant { 39 + amplitude: *stems[name].amplitude_db.get(self.ms).unwrap_or(&0.0), 40 + amplitude_max: stems[name].amplitude_max, 41 + velocity_max: stems[name] 42 + .notes 43 + .get(&self.ms) 44 + .iter() 45 + .map(|notes| notes.iter().map(|note| note.velocity).max().unwrap_or(0)) 46 + .max() 47 + .unwrap_or(0), 48 + duration: stems[name].duration_ms, 49 + notes: stems[name].notes.get(&self.ms).cloned().unwrap_or(vec![]), 50 + } 51 + } 52 + 53 + pub fn dump_syncdata(&self, to: PathBuf) -> anyhow::Result<()> { 54 + Ok(serde_cbor::to_writer(fs::File::create(to)?, self.syncdata)?) 55 + } 56 + 57 + pub fn marker(&self) -> String { 58 + self.syncdata 59 + .markers 60 + .get(&self.ms) 61 + .unwrap_or(&"".to_string()) 62 + .to_string() 63 + } 64 + 65 + pub fn duration_ms(&self) -> usize { 66 + match self.duration_override { 67 + Some(duration) => duration, 68 + None => self 69 + .syncdata 70 + .stems 71 + .values() 72 + .map(|stem| stem.duration_ms) 73 + .max() 74 + .unwrap(), 75 + } 76 + } 77 + 78 + pub fn later_frames(&mut self, delay: usize, render_function: &'static LaterRenderFunction) { 79 + let current_frame = self.frame; 80 + 81 + self.later_hooks.insert( 82 + 0, 83 + LaterHook { 84 + once: true, 85 + when: Box::new(move |_, context, _previous_beat| { 86 + context.frame >= current_frame + delay 87 + }), 88 + render_function: Box::new(render_function), 89 + }, 90 + ); 91 + } 92 + 93 + pub fn later_ms(&mut self, delay: usize, render_function: &'static LaterRenderFunction) { 94 + let current_ms = self.ms; 95 + 96 + self.later_hooks.insert( 97 + 0, 98 + LaterHook { 99 + once: true, 100 + when: Box::new(move |_, context, _previous_beat| context.ms >= current_ms + delay), 101 + render_function: Box::new(render_function), 102 + }, 103 + ); 104 + } 105 + 106 + pub fn later_beats(&mut self, delay: f32, render_function: &'static LaterRenderFunction) { 107 + let current_beat = self.beat; 108 + 109 + self.later_hooks.insert( 110 + 0, 111 + LaterHook { 112 + once: true, 113 + when: Box::new(move |_, context, _previous_beat| { 114 + context.beat_fractional >= current_beat as f32 + delay 115 + }), 116 + render_function: Box::new(render_function), 117 + }, 118 + ); 119 + } 120 + 121 + /// duration is in milliseconds 122 + pub fn start_animation(&mut self, duration: usize, animation: Animation) { 123 + let start_ms = self.ms; 124 + let ms_range = start_ms..(start_ms + duration); 125 + 126 + self.later_hooks.push(LaterHook { 127 + once: false, 128 + when: Box::new(move |_, ctx, _| ms_range.contains(&ctx.ms)), 129 + render_function: Box::new(move |canvas, ms| { 130 + let t = (ms - start_ms) as f32 / duration as f32; 131 + (animation.update)(t, canvas, ms) 132 + }), 133 + }) 134 + } 135 + 136 + /// duration is in milliseconds 137 + pub fn animate(&mut self, duration: usize, f: &'static AnimationUpdateFunction) { 138 + self.start_animation( 139 + duration, 140 + Animation::new(format!("unnamed animation {}", nanoid!()), f), 141 + ); 142 + } 143 + 144 + pub fn animate_layer( 145 + &mut self, 146 + layer: &'static str, 147 + duration: usize, 148 + f: &'static LayerAnimationUpdateFunction, 149 + ) { 150 + let animation = Animation { 151 + name: format!("unnamed animation {}", nanoid!()), 152 + update: Box::new(move |progress, canvas, ms| { 153 + (f)(progress, canvas.layer(layer), ms)?; 154 + canvas.layer(layer).flush(); 155 + Ok(()) 156 + }), 157 + }; 158 + 159 + self.start_animation(duration, animation); 160 + } 161 + }
+6
src/video/mod.rs
··· 1 + pub mod animation; 2 + pub mod context; 3 + pub mod video; 4 + 5 + pub use video::Video; 6 + pub use animation::Animation;
src/vst.rs src/vst/vst.rs
+30
src/wasm/transform.rs
··· 1 + use wasm_bindgen::prelude::*; 2 + 3 + #[wasm_bindgen(getter_with_clone)] 4 + #[derive(Debug, Clone)] 5 + pub struct TransformationWASM { 6 + pub kind: TransformationType, 7 + pub parameters: Vec<f32>, 8 + } 9 + 10 + impl From<TransformationWASM> for Transformation { 11 + fn from(transformation: TransformationWASM) -> Self { 12 + match transformation.kind { 13 + TransformationType::Scale => { 14 + Transformation::Scale(transformation.parameters[0], transformation.parameters[1]) 15 + } 16 + TransformationType::Rotate => Transformation::Rotate(transformation.parameters[0]), 17 + TransformationType::Skew => { 18 + Transformation::Skew(transformation.parameters[0], transformation.parameters[1]) 19 + } 20 + TransformationType::Matrix => Transformation::Matrix( 21 + transformation.parameters[0], 22 + transformation.parameters[1], 23 + transformation.parameters[2], 24 + transformation.parameters[3], 25 + transformation.parameters[4], 26 + transformation.parameters[5], 27 + ), 28 + } 29 + } 30 + }
src/web.rs src/wasm/web.rs