this repo has no description
3
fork

Configure Feed

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

✨ Add (wip) preview system via html, js and dumping SVG frames in the html

authored by

Gwenn Le Bihan and committed by
Ewen Le Bihan
263ec1ce a3e3d44d

+557 -113
+1
.gitignore
··· 13 13 fixtures/ 14 14 !fixtures/schedule-hell* 15 15 *.exe 16 + preview.html
+157
Cargo.lock
··· 39 39 checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" 40 40 41 41 [[package]] 42 + name = "block-buffer" 43 + version = "0.10.4" 44 + source = "registry+https://github.com/rust-lang/crates.io-index" 45 + checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 46 + dependencies = [ 47 + "generic-array", 48 + ] 49 + 50 + [[package]] 42 51 name = "bumpalo" 43 52 version = "3.16.0" 44 53 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 99 108 checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" 100 109 101 110 [[package]] 111 + name = "cpufeatures" 112 + version = "0.2.12" 113 + source = "registry+https://github.com/rust-lang/crates.io-index" 114 + checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" 115 + dependencies = [ 116 + "libc", 117 + ] 118 + 119 + [[package]] 102 120 name = "crossbeam-deque" 103 121 version = "0.8.5" 104 122 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 124 142 checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" 125 143 126 144 [[package]] 145 + name = "crypto-common" 146 + version = "0.1.6" 147 + source = "registry+https://github.com/rust-lang/crates.io-index" 148 + checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 149 + dependencies = [ 150 + "generic-array", 151 + "typenum", 152 + ] 153 + 154 + [[package]] 155 + name = "digest" 156 + version = "0.10.7" 157 + source = "registry+https://github.com/rust-lang/crates.io-index" 158 + checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 159 + dependencies = [ 160 + "block-buffer", 161 + "crypto-common", 162 + ] 163 + 164 + [[package]] 127 165 name = "docopt" 128 166 version = "1.1.1" 129 167 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 148 186 checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" 149 187 150 188 [[package]] 189 + name = "generic-array" 190 + version = "0.14.7" 191 + source = "registry+https://github.com/rust-lang/crates.io-index" 192 + checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 193 + dependencies = [ 194 + "typenum", 195 + "version_check", 196 + ] 197 + 198 + [[package]] 151 199 name = "getrandom" 152 200 version = "0.2.14" 153 201 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 163 211 version = "1.8.3" 164 212 source = "registry+https://github.com/rust-lang/crates.io-index" 165 213 checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403" 214 + 215 + [[package]] 216 + name = "handlebars" 217 + version = "5.1.2" 218 + source = "registry+https://github.com/rust-lang/crates.io-index" 219 + checksum = "d08485b96a0e6393e9e4d1b8d48cf74ad6c063cd905eb33f42c1ce3f0377539b" 220 + dependencies = [ 221 + "log", 222 + "pest", 223 + "pest_derive", 224 + "serde", 225 + "serde_json", 226 + "thiserror", 227 + ] 166 228 167 229 [[package]] 168 230 name = "hound" ··· 294 356 checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 295 357 296 358 [[package]] 359 + name = "pest" 360 + version = "2.7.9" 361 + source = "registry+https://github.com/rust-lang/crates.io-index" 362 + checksum = "311fb059dee1a7b802f036316d790138c613a4e8b180c822e3925a662e9f0c95" 363 + dependencies = [ 364 + "memchr", 365 + "thiserror", 366 + "ucd-trie", 367 + ] 368 + 369 + [[package]] 370 + name = "pest_derive" 371 + version = "2.7.9" 372 + source = "registry+https://github.com/rust-lang/crates.io-index" 373 + checksum = "f73541b156d32197eecda1a4014d7f868fd2bcb3c550d5386087cfba442bf69c" 374 + dependencies = [ 375 + "pest", 376 + "pest_generator", 377 + ] 378 + 379 + [[package]] 380 + name = "pest_generator" 381 + version = "2.7.9" 382 + source = "registry+https://github.com/rust-lang/crates.io-index" 383 + checksum = "c35eeed0a3fab112f75165fdc026b3913f4183133f19b49be773ac9ea966e8bd" 384 + dependencies = [ 385 + "pest", 386 + "pest_meta", 387 + "proc-macro2", 388 + "quote", 389 + "syn", 390 + ] 391 + 392 + [[package]] 393 + name = "pest_meta" 394 + version = "2.7.9" 395 + source = "registry+https://github.com/rust-lang/crates.io-index" 396 + checksum = "2adbf29bb9776f28caece835398781ab24435585fe0d4dc1374a61db5accedca" 397 + dependencies = [ 398 + "once_cell", 399 + "pest", 400 + "sha2", 401 + ] 402 + 403 + [[package]] 297 404 name = "portable-atomic" 298 405 version = "1.6.0" 299 406 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 456 563 ] 457 564 458 565 [[package]] 566 + name = "sha2" 567 + version = "0.10.8" 568 + source = "registry+https://github.com/rust-lang/crates.io-index" 569 + checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" 570 + dependencies = [ 571 + "cfg-if", 572 + "cpufeatures", 573 + "digest", 574 + ] 575 + 576 + [[package]] 459 577 name = "shapemaker" 460 578 version = "1.1.0" 461 579 dependencies = [ ··· 463 581 "chrono", 464 582 "chrono-human-duration", 465 583 "docopt", 584 + "handlebars", 466 585 "hound", 467 586 "indicatif", 468 587 "itertools", ··· 499 618 ] 500 619 501 620 [[package]] 621 + name = "thiserror" 622 + version = "1.0.59" 623 + source = "registry+https://github.com/rust-lang/crates.io-index" 624 + checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa" 625 + dependencies = [ 626 + "thiserror-impl", 627 + ] 628 + 629 + [[package]] 630 + name = "thiserror-impl" 631 + version = "1.0.59" 632 + source = "registry+https://github.com/rust-lang/crates.io-index" 633 + checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" 634 + dependencies = [ 635 + "proc-macro2", 636 + "quote", 637 + "syn", 638 + ] 639 + 640 + [[package]] 641 + name = "typenum" 642 + version = "1.17.0" 643 + source = "registry+https://github.com/rust-lang/crates.io-index" 644 + checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" 645 + 646 + [[package]] 647 + name = "ucd-trie" 648 + version = "0.1.6" 649 + source = "registry+https://github.com/rust-lang/crates.io-index" 650 + checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" 651 + 652 + [[package]] 502 653 name = "unicode-ident" 503 654 version = "1.0.12" 504 655 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 509 660 version = "0.1.12" 510 661 source = "registry+https://github.com/rust-lang/crates.io-index" 511 662 checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" 663 + 664 + [[package]] 665 + name = "version_check" 666 + version = "0.9.4" 667 + source = "registry+https://github.com/rust-lang/crates.io-index" 668 + checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 512 669 513 670 [[package]] 514 671 name = "wasi"
+1
Cargo.toml
··· 19 19 serde_json = "1.0.91" 20 20 svg = "0.17.0" 21 21 chrono-human-duration = "0.1.1" 22 + handlebars = "5.1.2" 22 23 23 24 [dev-dependencies] 24 25 rust-analyzer = "0.0.1"
+2 -2
Justfile
··· 5 5 install: 6 6 cp shapemaker ~/.local/bin/ 7 7 8 - example-video: 9 - ./shapemaker video --colors colorschemes/palenight.css out.mp4 --sync-with fixtures/schedule-hell.midi --audio fixtures/schedule-hell.flac --grid-size 16x10 8 + example-video args='': 9 + ./shapemaker video --colors colorschemes/palenight.css out.mp4 --sync-with fixtures/schedule-hell.midi --audio fixtures/schedule-hell.flac --grid-size 16x10 --resolution 1920 {{args}}
+1 -1
colorschemes/palenight.css
··· 4 4 red: #cf0a2b; 5 5 green: #22e753; 6 6 blue: #2734e6; 7 - yellow: #f7b337; 7 + yellow: #f8e21e; 8 8 orange: #f05811; 9 9 purple: #6a24ec; 10 10 brown: #a05634;
+64
preview/engine.js
··· 1 + function displayFrame() { 2 + const ms = millisecondsSinceStart(window.videoStartedAt) 3 + console.info("displayFrame", ms) 4 + 5 + if (window.previouslyRenderedFrame) { 6 + let f = window.frames.get(window.previouslyRenderedFrame) 7 + f.style.display = "none" 8 + f.classList.toggle("shown") 9 + } 10 + 11 + let frame = closestFrame(ms) 12 + f = window.frames.get(frame) 13 + f.style.display = "block" 14 + f.classList.toggle("shown") 15 + 16 + window.previouslyRenderedFrame = frame 17 + } 18 + 19 + function closestFrame(ms) { 20 + const closest = [...window.frames.keys()].reduce((a, b) => 21 + Math.abs(b - ms) < Math.abs(a - ms) ? b : a 22 + ) 23 + console.info("closestFrame", ms, "is", closest) 24 + return closest 25 + } 26 + 27 + /** 28 + * 29 + * @param {number} start 30 + * @returns 31 + */ 32 + function millisecondsSinceStart(start) { 33 + return new Date().getTime() - start 34 + } 35 + 36 + window.addEventListener("keypress", (e) => { 37 + if (e.key === " ") { 38 + if (window.intervalID) { 39 + stopVideo() 40 + } else { 41 + startVideo() 42 + } 43 + } 44 + }) 45 + 46 + window.startVideo = () => { 47 + window.frames = new Map( 48 + [...document.querySelectorAll("[id^=frame-]")].map((el) => [ 49 + parseInt(el.id.replace("frame-", "")), 50 + el, 51 + ]) 52 + ) 53 + window.refreshRate = 50 54 + window.videoStartedAt = new Date().getTime() 55 + window.previouslyRenderedFrame = null 56 + 57 + displayFrame() 58 + window.intervalID = setInterval(displayFrame, window.refreshRate) 59 + } 60 + 61 + window.stopVideo = () => { 62 + console.info("stopVideo", window.currentFrame) 63 + clearInterval(window.intervalID) 64 + }
+44
preview/index.html.hbs
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + 4 + <head> 5 + <meta charset="UTF-8"> 6 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 + <title>Shapemaker Preview</title> 8 + <script> 9 + {{{ enginesource }}} 10 + </script> 11 + {{!-- <script src="preview/engine.js"></script> --}} 12 + <style> 13 + .frame.shown { 14 + position: fixed; 15 + top: 0; 16 + left: 0; 17 + right: 0; 18 + bottom: 0; 19 + } 20 + 21 + .frame.shown>svg { 22 + width: 100%; 23 + height: 100%; 24 + } 25 + 26 + audio { 27 + position: fixed; 28 + z-index: 100; 29 + bottom: 0; 30 + left: 0; 31 + } 32 + </style> 33 + </head> 34 + 35 + <body style="background-color: {{background}};"> 36 + <audio src="{{ audiopath }}" controls onplay="startVideo()" onpause="stopVideo()"></audio> 37 + {{#each frames}} 38 + <div style="display: none;" id="frame-{{@key}}" class="frame"> 39 + {{{ this }}} 40 + </div> 41 + {{/each}} 42 + </body> 43 + 44 + </html>
+36 -6
src/canvas.rs
··· 49 49 Self::new(0, 0, end.0, end.1) 50 50 } 51 51 52 + pub fn from_origin_and_size(origin: (usize, usize), size: (usize, usize)) -> Self { 53 + Self::new( 54 + origin.0, 55 + origin.1, 56 + origin.0 + size.0 + 1, 57 + origin.1 + size.1 + 1, 58 + ) 59 + } 60 + 61 + pub fn from_center_and_size(center: (usize, usize), size: (usize, usize)) -> Self { 62 + let half_size = (size.0 / 2, size.1 / 2); 63 + Self::new( 64 + center.0 - half_size.0, 65 + center.1 - half_size.1, 66 + center.0 + half_size.0, 67 + center.1 + half_size.1, 68 + ) 69 + } 70 + 52 71 // panics if the region is invalid 53 - pub fn ensure_valid(&self) { 72 + pub fn ensure_valid(self) -> Self { 54 73 if self.start.0 >= self.end.0 || self.start.1 >= self.end.1 { 55 74 panic!( 56 75 "Invalid region: start ({:?}) >= end ({:?})", 57 76 self.start, self.end 58 77 ) 59 78 } 79 + self 60 80 } 61 81 62 82 pub fn translate(&mut self, dx: i32, dy: i32) { 63 - self.start.0 = (self.start.0 as i32 + dx) as usize; 64 - self.start.1 = (self.start.1 as i32 + dy) as usize; 65 - self.end.0 = (self.end.0 as i32 + dx) as usize; 66 - self.end.1 = (self.end.1 as i32 + dy) as usize; 67 - self.ensure_valid(); 83 + *self = self.translated(dx, dy); 84 + } 85 + 86 + pub fn translated(&self, dx: i32, dy: i32) -> Self { 87 + Self { 88 + start: ( 89 + (self.start.0 as i32 + dx) as usize, 90 + (self.start.1 as i32 + dy) as usize, 91 + ), 92 + end: ( 93 + (self.end.0 as i32 + dx) as usize, 94 + (self.end.1 as i32 + dy) as usize, 95 + ), 96 + } 97 + .ensure_valid() 68 98 } 69 99 70 100 pub fn x_range(&self) -> Range<usize> {
+4
src/cli.rs
··· 36 36 --workers <number> Number of parallel threads to use for rendering [default: 8] 37 37 --fps <fps> Frames per second [default: 30] 38 38 --audio <file> Audio file to use for the video 39 + --duration <seconds> Number of seconds to render. If not set, the video will be as long as the audio file. 40 + --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. 39 41 --sync-with <directory> Directory containing the audio files to sync to. 40 42 The directory must contain: 41 43 - stems/(instrument name).wav — stems ··· 89 91 pub flag_audio: Option<String>, 90 92 pub flag_resolution: Option<usize>, 91 93 pub flag_workers: Option<usize>, 94 + pub flag_duration: Option<usize>, 95 + pub flag_preview: bool, 92 96 } 93 97 94 98 fn set_canvas_settings_from_args(args: &Args, canvas: &mut Canvas) {
+5
src/layer.rs
··· 21 21 } 22 22 } 23 23 24 + // Flush the render cache. 25 + pub fn flush(&mut self) -> () { 26 + self._render_cache = None; 27 + } 28 + 24 29 pub fn replace(&mut self, with: Layer) -> () { 25 30 self.objects = with.objects.clone(); 26 31 self._render_cache = None;
+125 -70
src/lib.rs
··· 3 3 mod audio; 4 4 pub use audio::*; 5 5 mod sync; 6 + use itertools::Itertools; 7 + use midly::write; 6 8 use sync::SyncData; 7 9 pub use sync::Syncable; 8 10 mod layer; ··· 10 12 mod canvas; 11 13 pub use canvas::*; 12 14 mod midi; 15 + mod preview; 13 16 use anyhow::Result; 14 17 use chrono::NaiveDateTime; 15 18 use indicatif::{ProgressBar, ProgressStyle}; 16 19 pub use midi::MidiSynchronizer; 17 20 use std::cmp::min; 21 + use std::collections::HashMap; 18 22 use std::fmt::Formatter; 19 23 use std::fs::{self, create_dir, create_dir_all, remove_dir_all}; 20 24 use std::path::{Path, PathBuf}; 21 25 use std::sync::{Arc, Mutex}; 22 26 use std::thread::{self, JoinHandle}; 23 - use std::time; 27 + use std::{iter, time}; 24 28 25 29 const PROGRESS_BARS_STYLE: &str = 26 30 "{spinner:.cyan} {percent:03.bold.cyan}% {msg:<30} [{bar:100.bold.blue/dim.blue}] {eta:.cyan}"; ··· 47 51 pub syncdata: SyncData, 48 52 pub audiofile: PathBuf, 49 53 pub resolution: usize, 54 + pub duration_override: Option<usize>, 50 55 } 51 56 52 57 pub struct Hook<C> { ··· 93 98 pub audiofile: PathBuf, 94 99 pub later_hooks: Vec<LaterHook<AdditionalContext>>, 95 100 pub extra: AdditionalContext, 101 + pub duration_override: Option<usize>, 96 102 } 97 - 98 - const DURATION_OVERRIDE: Option<usize> = Some(2 * 60 * 1000); 99 103 100 104 pub trait GetOrDefault { 101 105 type Item; ··· 146 150 } 147 151 148 152 pub fn duration_ms(&self) -> usize { 149 - self.syncdata 150 - .stems 151 - .values() 152 - .map(|stem| stem.duration_ms) 153 - .map(|duration| { 154 - if let Some(duration_override) = DURATION_OVERRIDE { 155 - duration_override 156 - } else { 157 - duration 158 - } 159 - }) 160 - .max() 161 - .unwrap() 153 + match self.duration_override { 154 + Some(duration) => duration, 155 + None => self 156 + .syncdata 157 + .stems 158 + .values() 159 + .map(|stem| stem.duration_ms) 160 + .max() 161 + .unwrap(), 162 + } 162 163 } 163 164 164 165 pub fn later_frames(&mut self, delay: usize, render_function: &'static LaterRenderFunction<C>) { ··· 241 242 242 243 impl<AdditionalContext: Default> Default for Video<AdditionalContext> { 243 244 fn default() -> Self { 244 - Self::new() 245 + Self::new(Canvas::new(vec!["root"])) 245 246 } 246 247 } 247 248 248 249 impl<AdditionalContext: Default> Video<AdditionalContext> { 249 - pub fn new() -> Self { 250 + pub fn new(canvas: Canvas) -> Self { 250 251 Self { 251 252 fps: 30, 252 - initial_canvas: Canvas::new(vec!["root"]), 253 + initial_canvas: canvas, 253 254 hooks: vec![], 254 255 commands: vec![], 255 256 frames: vec![], ··· 257 258 resolution: 1000, 258 259 syncdata: SyncData::default(), 259 260 audiofile: PathBuf::new(), 260 - } 261 - } 262 - 263 - pub fn set_audio(self, final_audio: PathBuf) -> Self { 264 - Self { 265 - audiofile: final_audio, 266 - ..self 261 + duration_override: None, 267 262 } 268 263 } 269 264 ··· 329 324 ) 330 325 } 331 326 332 - pub fn set_fps(self, fps: usize) -> Self { 333 - Self { fps, ..self } 334 - } 335 - 336 - pub fn set_initial_canvas(self, canvas: Canvas) -> Self { 337 - Self { 338 - initial_canvas: canvas, 339 - ..self 340 - } 341 - } 342 - 343 327 pub fn init(self, render_function: &'static RenderFunction<AdditionalContext>) -> Self { 344 328 let hook = Hook { 345 329 when: Box::new(move |_, context, _, _| context.frame == 0), ··· 527 511 Self { hooks, ..self } 528 512 } 529 513 514 + pub fn when_remaining( 515 + self, 516 + seconds: usize, 517 + render_function: &'static RenderFunction<AdditionalContext>, 518 + ) -> Self { 519 + let hook = Hook { 520 + when: Box::new(move |_, ctx, _, _| { 521 + ctx.ms >= ctx.duration_ms().max(seconds * 1000) - seconds * 1000 522 + }), 523 + render_function: Box::new(render_function), 524 + }; 525 + let mut hooks = self.hooks; 526 + hooks.push(hook); 527 + Self { hooks, ..self } 528 + } 529 + 530 530 pub fn at_timestamp( 531 531 self, 532 532 timestamp: &'static str, ··· 604 604 } 605 605 606 606 pub fn duration_ms(&self) -> usize { 607 - if let Some(duration_override) = DURATION_OVERRIDE { 607 + if let Some(duration_override) = self.duration_override { 608 608 return duration_override; 609 609 } 610 610 ··· 616 616 .unwrap() 617 617 } 618 618 619 - pub fn render_to(&self, output_file: String, workers_count: usize) -> Result<&Self> { 620 - self.render_composition(output_file, vec!["*"], true, workers_count) 619 + // rendered_svg_frames should map ms timestamps to SVG strings 620 + pub fn output_preview( 621 + &self, 622 + canvas: &Canvas, 623 + rendered_svg_frames: HashMap<usize, String>, 624 + output_file: PathBuf, 625 + ) -> Result<&Self> { 626 + let contents = 627 + preview::render_template(rendered_svg_frames, canvas, self.audiofile.clone()); 628 + fs::write(output_file, contents)?; 629 + Ok(self) 630 + } 631 + 632 + pub fn render_to( 633 + &mut self, 634 + output_file: String, 635 + workers_count: usize, 636 + preview_only: bool, 637 + ) -> Result<&Self> { 638 + self.render_composition(output_file, vec!["*"], true, workers_count, preview_only) 621 639 } 622 640 623 641 pub fn render_layers_in( ··· 636 654 composition, 637 655 false, 638 656 workers_count, 657 + false, 639 658 )?; 640 659 } 641 660 Ok(self) 642 661 } 643 662 644 - pub fn render_composition( 663 + // Returns a triple of (SVG content, frame number, millisecond at frame) 664 + pub fn render_frames( 645 665 &self, 646 - output_file: String, 666 + progress_bar: &ProgressBar, 647 667 composition: Vec<&str>, 648 668 render_background: bool, 649 - workers_count: usize, 650 - ) -> Result<&Self> { 669 + ) -> Vec<(String, usize, usize)> { 651 670 let mut context = Context { 652 671 frame: 0, 653 672 beat: 0, ··· 659 678 extra: AdditionalContext::default(), 660 679 later_hooks: vec![], 661 680 audiofile: self.audiofile.clone(), 681 + duration_override: self.duration_override, 662 682 }; 663 683 664 684 let mut canvas = self.initial_canvas.clone(); 685 + 665 686 let mut previous_rendered_beat = 0; 666 687 let mut previous_rendered_frame = 0; 667 - 668 - let mut frame_writer_threads = vec![]; 669 - let mut frames_to_write: Vec<(String, usize)> = vec![]; 670 - 671 - remove_dir_all(self.frames_output_directory); 672 - create_dir(self.frames_output_directory).unwrap(); 673 - create_dir_all(Path::new(&output_file).parent().unwrap()).unwrap(); 674 - 675 - let progress_bar = indicatif::ProgressBar::new(self.total_frames() as u64).with_style( 676 - indicatif::ProgressStyle::with_template( 677 - &(PROGRESS_BARS_STYLE.to_owned() + " ({pos:.bold} frames out of {len})"), 678 - ) 679 - .unwrap() 680 - .progress_chars("== "), 681 - ); 682 - let total_frames = self.total_frames(); 683 - let aspect_ratio = canvas.grid_size.0 as f32 / canvas.grid_size.1 as f32; 684 - let resolution = self.resolution; 685 - let frames_output_directory = self.frames_output_directory; 686 - progress_bar.set_message("Rendering frames to SVG"); 688 + let mut frames_to_write: Vec<(String, usize, usize)> = vec![]; 687 689 688 690 for _ in 0..self.duration_ms() { 689 691 context.ms += 1_usize; ··· 743 745 744 746 if context.frame != previous_rendered_frame { 745 747 let rendered = canvas.render(&composition, render_background); 746 - std::fs::write( 747 - format!("{}/{}.svg", frames_output_directory, context.frame), 748 - &rendered, 749 - )?; 750 - frames_to_write.push((rendered, context.frame)); 748 + 751 749 previous_rendered_beat = context.beat; 752 750 previous_rendered_frame = context.frame; 753 751 progress_bar.inc(1); 752 + 753 + frames_to_write.push((rendered, context.frame, context.ms)) 754 754 } 755 755 } 756 756 757 + frames_to_write 758 + } 759 + 760 + pub fn render_composition( 761 + &self, 762 + output_file: String, 763 + composition: Vec<&str>, 764 + render_background: bool, 765 + workers_count: usize, 766 + preview_only: bool, 767 + ) -> Result<&Self> { 768 + let mut frame_writer_threads = vec![]; 769 + let mut frames_to_write: Vec<(String, usize, usize)> = vec![]; 770 + 771 + remove_dir_all(self.frames_output_directory); 772 + create_dir(self.frames_output_directory).unwrap(); 773 + create_dir_all(Path::new(&output_file).parent().unwrap()).unwrap(); 774 + 775 + let progress_bar = indicatif::ProgressBar::new(self.total_frames() as u64).with_style( 776 + indicatif::ProgressStyle::with_template( 777 + &(PROGRESS_BARS_STYLE.to_owned() + " ({pos:.bold} frames out of {len})"), 778 + ) 779 + .unwrap() 780 + .progress_chars("== "), 781 + ); 782 + let total_frames = self.total_frames(); 783 + let aspect_ratio = 784 + self.initial_canvas.grid_size.0 as f32 / self.initial_canvas.grid_size.1 as f32; 785 + let resolution = self.resolution; 786 + let frames_output_directory = self.frames_output_directory; 787 + progress_bar.set_message("Rendering frames to SVG"); 788 + 789 + for (frame, no, ms) in self.render_frames(&progress_bar, composition, render_background) { 790 + std::fs::write(format!("{}/{}.svg", frames_output_directory, no), &frame)?; 791 + frames_to_write.push((frame, no, ms)); 792 + } 793 + 794 + self.output_preview( 795 + &self.initial_canvas, 796 + frames_to_write 797 + .iter() 798 + .map(|(f, _, ms)| (ms.clone(), f.clone())) 799 + .collect(), 800 + PathBuf::from(&output_file) 801 + .parent() 802 + .unwrap() 803 + .join("preview.html"), 804 + ); 805 + 757 806 progress_bar.println("Rendered frames to SVG"); 807 + 808 + if preview_only { 809 + progress_bar.finish_and_clear(); 810 + return Ok(self); 811 + } 812 + 758 813 progress_bar.set_message("Rendering SVG frames to PNG"); 759 814 progress_bar.set_position(0); 760 815 ··· 767 822 thread::Builder::new() 768 823 .name(format!("worker-{}", i)) 769 824 .spawn(move || { 770 - for (frame_svg, frame_no) in &frames_to_write 825 + for (frame_svg, frame_no, _) in &frames_to_write 771 826 [i * chunk_size..min((i + 1) * chunk_size, frames_to_write.len())] 772 827 { 773 828 Video::<AdditionalContext>::build_frame(
+90 -34
src/main.rs
··· 1 - use shapemaker::{Canvas, Color, Fill, Object, Region, Video}; 1 + use shapemaker::{Canvas, Color, Fill, Layer, Object, Region, Video}; 2 2 mod cli; 3 3 pub use cli::{canvas_from_cli, cli_args}; 4 4 ··· 22 22 return; 23 23 } 24 24 25 - Video::<State>::new() 26 - .set_fps(args.flag_fps.unwrap_or(30)) 27 - .set_initial_canvas(canvas) 25 + let mut video = Video::<State>::new(canvas); 26 + video.duration_override = args.flag_duration.map(|seconds| seconds * 1000); 27 + video.fps = args.flag_fps.unwrap_or(30); 28 + video.audiofile = args.flag_audio.unwrap().into(); 29 + 30 + video 28 31 .init(&|canvas: _, context: _| { 29 32 context.extra = State { 30 - kick_region: Region::new(2, 2, 4, 4), 33 + bass_pattern_at: Region::from_origin_and_size((6, 3), (3, 3)), 34 + first_kick_happened: false, 31 35 }; 32 36 canvas.set_background(Color::Black); 33 37 }) 34 - .set_audio(args.flag_audio.unwrap().into()) 35 38 .sync_audio_with(&args.flag_sync_with.unwrap()) 39 + .on_note("anchor kick", &|_, ctx| { 40 + // ctx.extra.bass_pattern_at = region_cycle(&canvas.world_region, None); 41 + ctx.extra.first_kick_happened = true; 42 + }) 36 43 .on_note("bass", &|canvas, ctx| { 37 - let mut new_layer = canvas.random_layer_within("bass", &ctx.extra.kick_region); 44 + let mut new_layer = canvas.random_layer_within("bass", &ctx.extra.bass_pattern_at); 38 45 new_layer.paint_all_objects(Fill::Solid(Color::White)); 39 46 canvas.replace_or_create_layer("bass", new_layer); 40 47 }) 41 - .on_note("anchor kick", &|canvas, ctx| { 42 - let new_kick_region = region_cycle(&canvas.world_region, &ctx.extra.kick_region); 43 - 44 - let (dx, dy) = new_kick_region - ctx.extra.kick_region; 45 - canvas.layer("bass").unwrap().move_all_objects(dx, dy); 46 - 47 - ctx.extra.kick_region = new_kick_region; 48 - }) 49 48 .on_note("powerful clap hit, clap, perclap", &|canvas, ctx| { 50 - let mut new_layer = canvas.random_layer_within( 51 - "claps", 52 - &region_cycle(&canvas.world_region, &ctx.extra.kick_region), 53 - ); 49 + let mut new_layer = 50 + canvas.random_layer_within("claps", &ctx.extra.bass_pattern_at.translated(2, 0)); 54 51 new_layer.paint_all_objects(Fill::Solid(Color::Red)); 55 52 canvas.replace_or_create_layer("claps", new_layer) 56 53 }) 57 - .on("start credits", &|canvas, _| { 54 + .on_note("qanda", &|canvas, ctx| { 55 + if ctx.stem("qanda").amplitude_relative() < 0.7 { 56 + return; 57 + } 58 + 59 + let mut new_layer = 60 + canvas.random_layer_within("qanda", &ctx.extra.bass_pattern_at.translated(-2, 0)); 61 + new_layer.paint_all_objects(Fill::Solid(Color::Orange)); 62 + canvas.replace_or_create_layer("qanda", new_layer) 63 + }) 64 + .on_note("brokenup", &|canvas, ctx| { 65 + let mut new_layer = canvas 66 + .random_layer_within("brokenup", &ctx.extra.bass_pattern_at.translated(0, -2)); 67 + new_layer.paint_all_objects(Fill::Solid(Color::Yellow)); 68 + canvas.replace_or_create_layer("brokenup", new_layer); 69 + }) 70 + .on_note("goup", &|canvas, ctx| { 71 + let mut new_layer = 72 + canvas.random_layer_within("goup", &ctx.extra.bass_pattern_at.translated(0, 2)); 73 + new_layer.paint_all_objects(Fill::Solid(Color::Yellow)); 74 + canvas.replace_or_create_layer("goup", new_layer); 75 + }) 76 + .when_remaining(10, &|canvas, _| { 58 77 canvas.root().add_object( 59 78 "credits text", 60 79 Object::RawSVG(Box::new(svg::node::Text::new("by ewen-lbh"))), 61 80 None, 62 81 ); 63 - }) 64 - .on("end credits", &|canvas, _| { 65 - canvas.remove_object("credits text"); 66 82 }) 67 83 .command("remove", &|argumentsline, canvas, _| { 68 84 let args = argumentsline.splitn(3, ' ').collect::<Vec<_>>(); 69 85 canvas.remove_object(args[0]); 70 86 }) 71 - .render_to(args.arg_file, args.flag_workers.unwrap_or(8)) 87 + .render_to( 88 + args.arg_file, 89 + args.flag_workers.unwrap_or(8), 90 + args.flag_preview, 91 + ) 72 92 .unwrap(); 73 93 } 74 94 95 + fn update_stem_position( 96 + ctx: &mut shapemaker::Context<State>, 97 + canvas: &mut Canvas, 98 + layer_name: &str, 99 + offset: usize, 100 + ) { 101 + let (dx, dy) = ctx.extra.bass_pattern_at 102 + - region_cycle_with_offset( 103 + &canvas.world_region, 104 + Some(&ctx.extra.bass_pattern_at), 105 + offset, 106 + ); 107 + match canvas.layer(layer_name) { 108 + Some(l) => l.move_all_objects(dx, dy), 109 + _ => (), 110 + } 111 + } 112 + 75 113 #[derive(Default)] 76 114 struct State { 77 - kick_region: Region, 115 + first_kick_happened: bool, 116 + bass_pattern_at: Region, 78 117 } 79 118 80 119 fn color_cycle(current_color: Color) -> Color { ··· 92 131 } 93 132 } 94 133 95 - fn region_cycle(world: &Region, current: &Region) -> Region { 96 - let size = (current.width(), current.height()); 97 - let mut new_region = current.clone(); 134 + fn region_cycle_with_offset(world: &Region, current: Option<&Region>, offset: usize) -> Region { 135 + if offset == 0 { 136 + return current.unwrap().clone(); 137 + } 138 + 139 + if offset == 1 { 140 + return region_cycle(world, current); 141 + } 142 + 143 + region_cycle_with_offset(world, current, offset - 1) 144 + } 145 + 146 + fn region_cycle(world: &Region, current: Option<&Region>) -> Region { 147 + let mut region = if let Some(current) = current { 148 + current.clone() 149 + } else { 150 + Region::from_origin_and_size((1, 1), (2, 2)) 151 + }; 152 + 153 + let size = (region.width(), region.height()); 98 154 // Move along x axis if possible 99 - if current.end.0 + size.0 <= world.end.0 { 100 - new_region.translate(size.0 as i32, 0) 155 + if region.end.0 + size.0 <= world.end.0 - 1 { 156 + region.translate(size.0 as i32, 0) 101 157 } 102 158 // Else go to x=0 and move along y axis 103 - else if current.end.1 + size.1 <= world.end.1 { 104 - new_region = Region::new(2, current.end.1, size.0 + 2, current.end.1 + size.1) 159 + else if region.end.1 + size.1 <= world.end.1 - 1 { 160 + region = Region::new(2, region.end.1, size.0 + 2, region.end.1 + size.1) 105 161 } 106 162 // Else go to origin 107 163 else { 108 - new_region = Region::from_origin(size) 164 + region = Region::from_origin_and_size((1, 1), size) 109 165 } 110 - new_region 166 + region 111 167 }
+27
src/preview.rs
··· 1 + use std::{collections::HashMap, path::PathBuf}; 2 + 3 + use handlebars::Handlebars; 4 + use serde_json::json; 5 + 6 + use crate::{Canvas, ColorMapping}; 7 + 8 + pub fn render_template( 9 + frames: HashMap<usize, String>, 10 + canvas: &Canvas, 11 + path_to_audio_file: PathBuf, 12 + ) -> String { 13 + let template = String::from_utf8_lossy(include_bytes!("../preview/index.html.hbs")); 14 + let engine_js_source = String::from_utf8_lossy(include_bytes!("../preview/engine.js")); 15 + 16 + let mut hbs = Handlebars::new(); 17 + hbs.render_template( 18 + &template, 19 + &json!({ 20 + "frames":frames, 21 + "audiopath": path_to_audio_file, 22 + "enginesource": engine_js_source, 23 + "background": canvas.background.map_or("black".to_string(), |color| color.to_string(&canvas.colormap)) 24 + }), 25 + ) 26 + .unwrap() 27 + }