···11use core::panic;
22-use std::{collections::HashMap, io::Write as _, ops::Range};
22+use std::{collections::HashMap, ops::Range};
3344-use anyhow::Result;
54use itertools::Itertools as _;
55+use measure_time::info_time;
66use rand::Rng;
7788use crate::{
···2424 pub background: Option<Color>,
25252626 pub world_region: Region,
2727+2828+ /// Render cache for the SVG string. Prevents having to re-calculate a pixmap when the SVG hasn't changed.
2929+ png_render_cache: Option<String>,
2730}
28312932impl Canvas {
···187190 layers: vec![],
188191 world_region: Region::new(0, 0, 3, 3).unwrap(),
189192 background: None,
193193+ png_render_cache: None,
190194 }
191195 }
192196···414418 self.remove_background()
415419 }
416420417417- pub fn save_as(
421421+ // previous_frame_at gives path to the previously rendered frame, which allows to copy on cache hits instead of having to re-write bytes again
422422+ pub fn render_to_png(
423423+ &mut self,
418424 at: &str,
419419- aspect_ratio: f32,
420420- resolution: usize,
421421- rendered: String,
422422- ) -> Result<(), String> {
425425+ resolution: u32,
426426+ previous_frame_at: Option<&str>,
427427+ ) -> anyhow::Result<()> {
428428+ info_time!("render_to_png");
429429+ let aspect_ratio = self.aspect_ratio();
423430 let (height, width) = if aspect_ratio > 1.0 {
424431 // landscape: resolution is width
425425- (resolution, (resolution as f32 * aspect_ratio) as usize)
432432+ (resolution, (resolution as f32 * aspect_ratio) as u32)
426433 } else {
427434 // portrait: resolution is height
428428- ((resolution as f32 / aspect_ratio) as usize, resolution)
435435+ ((resolution as f32 / aspect_ratio) as u32, resolution)
429436 };
430437431431- let mut spawned = std::process::Command::new("resvg")
432432- .args(["--background", "transparent"])
433433- .args(["--width", &format!("{width}")])
434434- .args(["--height", &format!("{height}")])
435435- .args(["--use-font-file", "Inconsolata-Bold.ttf"])
436436- .args(["--resources-dir", "."])
437437- .arg("-")
438438- .arg(at)
439439- .stdin(std::process::Stdio::piped())
440440- .spawn()
441441- .unwrap();
438438+ if let Some(previous_frame_at) = previous_frame_at {
439439+ match self.render_to_pixmap(width, height)? {
440440+ None => {
441441+ std::fs::copy(previous_frame_at, at)?;
442442+ }
443443+ Some(pixmap) => {
444444+ pixmap_to_png_data(pixmap).and_then(|data| write_png_data(data, at))?
445445+ }
446446+ }
447447+ return Ok(());
448448+ }
449449+450450+ Ok(self
451451+ .render_to_pixmap_no_cache(width, height)
452452+ .and_then(|pixmap| {
453453+ pixmap_to_png_data(pixmap).and_then(|data| write_png_data(data, at))
454454+ })?)
455455+ }
456456+}
442457443443- let stdin = spawned.stdin.as_mut().unwrap();
444444- stdin.write_all(rendered.as_bytes()).unwrap();
458458+fn pixmap_to_png_data(pixmap: tiny_skia::Pixmap) -> anyhow::Result<Vec<u8>> {
459459+ info_time!("\tpixmap_to_png_data");
460460+ Ok(pixmap.encode_png()?)
461461+}
445462446446- match spawned.wait_with_output() {
447447- Ok(_) => Ok(()),
448448- Err(e) => Err(format!("Failed to execute convert: {}", e)),
449449- }
450450- }
463463+fn write_png_data(data: Vec<u8>, at: &str) -> anyhow::Result<()> {
464464+ info_time!("\twrite_png_data");
465465+ std::fs::write(at, data)?;
466466+ Ok(())
451467}
452468453469impl Canvas {
···514530 )
515531 }
516532517517- pub fn render(&mut self, render_background: bool) -> Result<String> {
533533+ pub fn render_to_svg(&mut self) -> anyhow::Result<String> {
534534+ info_time!("render_to_svg");
518535 let background_color = self.background.unwrap_or_default();
519536 let mut svg = svg::Document::new();
520520- if render_background {
521521- svg = svg.add(
522522- svg::node::element::Rectangle::new()
523523- .set("x", -(self.canvas_outter_padding as i32))
524524- .set("y", -(self.canvas_outter_padding as i32))
525525- .set("width", self.width())
526526- .set("height", self.height())
527527- .set("fill", background_color.render(&self.colormap)),
528528- );
529529- }
537537+ svg = svg.add(
538538+ svg::node::element::Rectangle::new()
539539+ .set("x", -(self.canvas_outter_padding as i32))
540540+ .set("y", -(self.canvas_outter_padding as i32))
541541+ .set("width", self.width())
542542+ .set("height", self.height())
543543+ .set("fill", background_color.render(&self.colormap)),
544544+ );
545545+530546 for layer in self.layers.iter_mut().filter(|layer| !layer.hidden).rev() {
531547 svg = svg.add(layer.render(self.colormap.clone(), self.cell_size, layer.object_sizes));
532548 }
···559575560576 Ok(rendered)
561577 }
578578+579579+ pub fn render_to_pixmap_no_cache(
580580+ &mut self,
581581+ width: u32,
582582+ height: u32,
583583+ ) -> anyhow::Result<tiny_skia::Pixmap> {
584584+ info_time!("render_to_pixmap_no_cache");
585585+ let mut pixmap = self.create_pixmap(width, height);
586586+587587+ let parsed_svg = &svg_to_usvg_tree(&self.render_to_svg()?)?;
588588+589589+ self.usvg_tree_to_pixmap(width, height, pixmap.as_mut(), parsed_svg);
590590+591591+ Ok(pixmap)
592592+ }
593593+594594+ // Returns None if we had a render cache hit -- pixmap is in self.png_render_cache in that case
595595+ pub fn render_to_pixmap(
596596+ &mut self,
597597+ width: u32,
598598+ height: u32,
599599+ ) -> anyhow::Result<Option<tiny_skia::Pixmap>> {
600600+ info_time!("render_to_pixmap");
601601+602602+ let new_svg_contents = self.render_to_svg()?;
603603+ if let Some(cached_svg) = &self.png_render_cache {
604604+ if *cached_svg == new_svg_contents {
605605+ // TODO find a way to avoid .cloneing the pixmap
606606+ return Ok(None);
607607+ }
608608+ }
609609+610610+ let mut pixmap = self.create_pixmap(width, height);
611611+612612+ let parsed_svg = &svg_to_usvg_tree(&new_svg_contents)?;
613613+614614+ self.usvg_tree_to_pixmap(width, height, pixmap.as_mut(), parsed_svg);
615615+616616+ self.png_render_cache = Some(new_svg_contents);
617617+618618+ Ok(Some(pixmap))
619619+ }
620620+621621+ fn usvg_tree_to_pixmap(
622622+ &mut self,
623623+ width: u32,
624624+ height: u32,
625625+ mut pixmap_mut: tiny_skia::PixmapMut<'_>,
626626+ parsed_svg: &resvg::usvg::Tree,
627627+ ) {
628628+ info_time!("usvg_tree_to_pixmap");
629629+ resvg::render(
630630+ parsed_svg,
631631+ tiny_skia::Transform::from_scale(
632632+ width as f32 / self.width() as f32,
633633+ height as f32 / self.height() as f32,
634634+ ),
635635+ &mut pixmap_mut,
636636+ );
637637+ }
638638+639639+ fn create_pixmap(&self, width: u32, height: u32) -> tiny_skia::Pixmap {
640640+ info_time!("create_pixmap");
641641+ tiny_skia::Pixmap::new(width, height).expect("Failed to create pixmap")
642642+ }
643643+}
644644+645645+fn svg_to_usvg_tree(svg: &str) -> anyhow::Result<resvg::usvg::Tree> {
646646+ info_time!("svg_to_usvg_tree");
647647+ Ok(resvg::usvg::Tree::from_str(
648648+ svg,
649649+ &resvg::usvg::Options::default(),
650650+ )?)
562651}
+3-3
src/cli.rs
···11use docopt::Docopt;
22+use measure_time::info_time;
23use serde::Deserialize;
34use crate::{Canvas, ColorMapping};
45···3839 --audio <file> Audio file to use for the video
3940 --duration <seconds> Number of seconds to render. If not set, the video will be as long as the audio file.
4041 --start <seconds> Start the video at this time in seconds. [default: 0]
4141- --preview Only create preview.html, not the output video. Preview.html will be created in the same directory as <file>, but <file> will not be created.
4242 --sync-with <directory> Directory containing the audio files to sync to.
4343 The directory must contain:
4444 - stems/(instrument name).wav — stems
···6363}
64646565pub fn canvas_from_cli(args: &Args) -> Canvas {
6666+ info_time!("canvas_from_cli");
6667 let mut canvas = Canvas::new(vec![]);
6768 canvas.colormap = load_colormap(args);
6869 set_canvas_settings_from_args(args, &mut canvas);
···9091 pub flag_fps: Option<usize>,
9192 pub flag_sync_with: Option<String>,
9293 pub flag_audio: Option<String>,
9393- pub flag_resolution: Option<usize>,
9494+ pub flag_resolution: Option<u32>,
9495 pub flag_workers: Option<usize>,
9596 pub flag_duration: Option<usize>,
9697 pub flag_start: Option<usize>,
9797- pub flag_preview: bool,
9898}
9999100100fn set_canvas_settings_from_args(args: &Args, canvas: &mut Canvas) {
-1
src/lib.rs
···1212pub mod midi;
1313pub mod objects;
1414pub mod point;
1515-pub mod preview;
1615pub mod region;
1716pub mod sync;
1817pub mod transform;